New Upstream Release - ruby-paper-trail

Ready changes

Summary

Merged new upstream version: 14.0.0 (was: 12.0.0).

Resulting package

Built on 2023-01-20T06:34 (took 3m16s)

The resulting binary packages can be installed (if you have the apt repository enabled) by running one of:

apt install -t fresh-releases ruby-paper-trail

Lintian Result

Diff

diff --git a/.github/CONTRIBUTING.md b/.github/CONTRIBUTING.md
index d15c698..960cfc9 100644
--- a/.github/CONTRIBUTING.md
+++ b/.github/CONTRIBUTING.md
@@ -48,29 +48,18 @@ Testing is a little awkward because the test suite:
 1. Contains a "dummy" rails app with three databases (test, foo, and bar)
 1. Supports three different RDBMS': sqlite, mysql, and postgres
 
-### Test sqlite, AR 6
+### Test
 
-```
-DB=sqlite bundle exec appraisal rails-6.0 rake
-```
-
-### Test sqlite, AR 5
-
-```
-DB=sqlite bundle exec appraisal rails-5.2 rake
-```
-
-### Test mysql, AR 5
-
-```
-DB=mysql bundle exec appraisal rails-5.2 rake
-```
-
-### Test postgres, AR 5
+For most development, testing with sqlite only is easiest and sufficient. CI
+will run the rest.
 
 ```
+DB=sqlite bundle exec appraisal rails-6.0 rake
+DB=sqlite bundle exec appraisal rails-6.1 rake
+DB=sqlite bundle exec appraisal rails-7.0 rake
+DB=mysql bundle exec appraisal rails-7.0 rake
 createuser --superuser postgres
-DB=postgres bundle exec appraisal rails-5.2 rake
+DB=postgres bundle exec appraisal rails-7.0 rake
 ```
 
 ## The dummy_app
@@ -80,7 +69,7 @@ In the rare event you need a `console` in the `dummy_app`:
 ```
 cd spec/dummy_app
 cp config/database.mysql.yml config/database.yml
-BUNDLE_GEMFILE='../../gemfiles/rails_5.2.gemfile' bin/rails console -e test
+BUNDLE_GEMFILE='../../gemfiles/rails_7.0.gemfile' bin/rails console -e test
 ```
 
 ## Adding new schema
@@ -139,4 +128,4 @@ markdown-toc -i --maxdepth 3 --bullets='-' README.md
   1. cherry-pick the "Release 10.3.0" commit from the `10-stable` branch
   1. git push origin master
 
-[1]: https://github.com/paper-trail-gem/paper_trail/blob/master/.github/ISSUE_TEMPLATE/bug_report.md
+[1]: https://github.com/paper-trail-gem/paper_trail/blob/master/.github/ISSUE_TEMPLATE/bug-report.md
diff --git a/.github/ISSUE_TEMPLATE/bug-report.md b/.github/ISSUE_TEMPLATE/bug-report.md
index dff3b54..4122303 100644
--- a/.github/ISSUE_TEMPLATE/bug-report.md
+++ b/.github/ISSUE_TEMPLATE/bug-report.md
@@ -32,11 +32,11 @@ require "bundler/inline"
 
 # STEP ONE: What versions are you using?
 gemfile(true) do
-  ruby "2.5.1"
+  ruby "3.0.2"
   source "https://rubygems.org"
-  gem "activerecord", "5.2.0"
+  gem "activerecord", "6.1.4.1"
   gem "minitest", "5.11.3"
-  gem "paper_trail", "9.2.0", require: false
+  gem "paper_trail", "12.1.0", require: false
   gem "sqlite3", "1.3.13"
 end
 
diff --git a/.github/ISSUE_TEMPLATE/feature-suggestion.md b/.github/ISSUE_TEMPLATE/feature-suggestion.md
index a3b93d8..be94590 100644
--- a/.github/ISSUE_TEMPLATE/feature-suggestion.md
+++ b/.github/ISSUE_TEMPLATE/feature-suggestion.md
@@ -15,7 +15,7 @@ closed without comment.
 **Is your feature suggestion related to a problem? Please describe.**
 
 A clear and concise description of the problem. You may find the
-[bug report template](https://github.com/paper-trail-gem/paper_trail/blob/master/.github/ISSUE_TEMPLATE/bug_report.md)
+[bug report template](https://github.com/paper-trail-gem/paper_trail/blob/master/.github/ISSUE_TEMPLATE/bug-report.md)
 helpful.
 
 **Describe the solution you'd like to build**
diff --git a/.github/workflows/stale.yml b/.github/workflows/stale.yml
index 15841af..9d64563 100644
--- a/.github/workflows/stale.yml
+++ b/.github/workflows/stale.yml
@@ -3,12 +3,19 @@ on:
   schedule:
     - cron: '30 1 * * *'
 
+permissions:
+  contents: read
+
 jobs:
   stale:
+    permissions:
+      issues: write  # for actions/stale to close stale issues
+      pull-requests: write  # for actions/stale to close stale PRs
     runs-on: ubuntu-latest
     steps:
-      - uses: actions/stale@v3
+      - uses: actions/stale@v6
         with:
+          exempt-issue-labels: keep
           stale-issue-message: >
             This issue has been automatically marked as stale due to inactivity.
 
diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml
index 8ecdb41..cb1bd54 100644
--- a/.github/workflows/test.yml
+++ b/.github/workflows/test.yml
@@ -1,5 +1,8 @@
 name: gha-workflow-pt-test
 on: [push, pull_request]
+permissions:
+  contents: read
+
 jobs:
 
   # Linting is a separate job, primary because it only needs to be done once,
@@ -14,7 +17,7 @@ jobs:
         uses: ruby/setup-ruby@v1
         with:
           # See "Lowest supported ruby version" in CONTRIBUTING.md
-          ruby-version: '2.5'
+          ruby-version: '2.7'
       - name: Bundle
         run: |
           gem install bundler
@@ -56,20 +59,14 @@ jobs:
       # have set this up with each database as a separate job, but then we'd be
       # duplicating the matrix configuration three times.
       matrix:
-        gemfile: [ 'rails_5.2', 'rails_6.0', 'rails_6.1' ]
+        gemfile: [ 'rails_6.0', 'rails_6.1', 'rails_7.0' ]
 
         # To keep matrix size down, only test highest and lowest rubies.
         # Ruby 3.0 is an exception. For now, let's continue to test against 2.7
         # in case it still produces any deprecation warnings.
         #
         # See "Lowest supported ruby version" in CONTRIBUTING.md
-        ruby: [ '2.5', '2.7', '3.0' ]
-
-        exclude:
-          # rails 5.2 requires ruby < 3.0
-          # https://github.com/rails/rails/issues/40938
-          - ruby: '3.0'
-            gemfile: 'rails_5.2'
+        ruby: [ '2.7', '3.0', '3.1' ]
     steps:
       - name: Checkout source
         uses: actions/checkout@v2
@@ -77,10 +74,7 @@ jobs:
         uses: ruby/setup-ruby@v1
         with:
           ruby-version: ${{ matrix.ruby }}
-      - name: Bundle
-        run: |
-          gem install bundler
-          bundle install --jobs 4 --retry 3
+          bundler-cache: true # 'bundle install' and cache
         env:
           BUNDLE_GEMFILE: gemfiles/${{ matrix.gemfile }}.gemfile
 
diff --git a/.rubocop.yml b/.rubocop.yml
index 1f7d143..a99e963 100644
--- a/.rubocop.yml
+++ b/.rubocop.yml
@@ -2,6 +2,7 @@ require:
   - rubocop-packaging
   - rubocop-performance
   - rubocop-rails
+  - rubocop-rake
   - rubocop-rspec
 
 inherit_from: .rubocop_todo.yml
@@ -13,25 +14,24 @@ inherit_from: .rubocop_todo.yml
 # - Only include permanent config; temporary goes in .rubocop_todo.yml
 
 AllCops:
+  # Generated files, like schema.rb, are out of our control.
   Exclude:
-    - gemfiles/vendor/bundle/**/* # This dir only shows up on travis ¯\_(ツ)_/¯
-    - spec/dummy_app/db/schema.rb # Generated, out of our control
+    - gemfiles/*
+    - spec/dummy_app/db/schema.rb
 
   # Enable pending cops so we can adopt the code before they are switched on.
   NewCops: enable
 
   # See "Lowest supported ruby version" in CONTRIBUTING.md
-  TargetRubyVersion: 2.5
-
-Bundler/OrderedGems:
-  Exclude:
-    - gemfiles/* # generated by Appraisal
+  TargetRubyVersion: 2.7
 
 Layout/ArgumentAlignment:
   EnforcedStyle: with_fixed_indentation
 
+# This cop has a bug in 1.22.2 (https://github.com/rubocop/rubocop/issues/10210)
+# When the bug is fixed, we'll return to using `EnforcedStyle: trailing`.
 Layout/DotPosition:
-  EnforcedStyle: trailing
+  Enabled: false
 
 # Avoid blank lines inside methods. They are a sign that the method is too big.
 Layout/EmptyLineAfterGuardClause:
@@ -56,20 +56,11 @@ Layout/MultilineOperationIndentation:
 Layout/ParameterAlignment:
   EnforcedStyle: with_fixed_indentation
 
-Layout/SpaceAroundMethodCallOperator:
-  Enabled: true
-
 # Use exactly one space on each side of an operator. Do not align operators
 # because it makes the code harder to edit, and makes lines unnecessarily long.
 Layout/SpaceAroundOperators:
   AllowForAlignment: false
 
-Lint/RaiseException:
-  Enabled: true
-
-Lint/StructNewOverride:
-  Enabled: true
-
 # Migrations often contain long up/down methods, and extracting smaller methods
 # from these is of questionable value.
 Metrics/AbcSize:
@@ -99,12 +90,9 @@ Naming/FileName:
     - Appraisals
 
 # Heredocs are usually assigned to a variable or constant, which already has a
-# name, so naming the heredoc doesn't add much value. Feel free to name
-# heredocs that are used as anonymous values (not a variable, constant, or
-# named parameter).
-#
-# All heredocs containing SQL should be named SQL, to support editor syntax
-# highlighting.
+# name, so naming the delimiter doesn't add much value unless doing so improves
+# syntax highlighting. For example, all heredocs containing SQL should be named
+# SQL, to support editor syntax highlighting.
 Naming/HeredocDelimiterNaming:
   Enabled: false
 
@@ -117,13 +105,26 @@ Naming/PredicateName:
 Naming/MethodParameterName:
   Enabled: false
 
-# This cop does not seem to work in rubocop-rspec 1.28.0
-RSpec/DescribeClass:
+# This cop has low value to begin with. Also, secondarily, it does not allow
+# reasonable names like `rails_lt_6_0`.
+Naming/VariableNumber:
   Enabled: false
 
-# This cop has a bug in 1.35.0
-# https://github.com/rubocop-hq/rubocop-rspec/issues/799
-RSpec/DescribedClass:
+# A valuable optimization in production code, but not valuable in specs.
+Performance/CollectionLiteralInLoop:
+  Exclude:
+    - spec/**/*
+
+# This cop only applies to app dev, not gem dev.
+Rails/RakeEnvironment:
+  Enabled: false
+
+# Good advice for rails applications, but not applicable to libraries like PT.
+Rails/SkipsModelValidations:
+  Enabled: false
+
+# This cop does not seem to work in rubocop-rspec 1.28.0
+RSpec/DescribeClass:
   Enabled: false
 
 # Yes, ideally examples would be short. Is it possible to pick a limit and say,
@@ -149,10 +150,6 @@ Style/BlockDelimiters:
 Style/DoubleNegation:
   Enabled: false
 
-# This cop is unimportant in this repo.
-Style/ExponentialNotation:
-  Enabled: false
-
 # Avoid annotated tokens except in desperately complicated format strings.
 # In 99% of format strings they actually make it less readable.
 Style/FormatStringToken:
diff --git a/.rubocop_todo.yml b/.rubocop_todo.yml
index 03f21de..4c60969 100644
--- a/.rubocop_todo.yml
+++ b/.rubocop_todo.yml
@@ -1,129 +1,23 @@
 # This configuration was generated by
 # `rubocop --auto-gen-config`
-# on 2021-03-21 03:46:53 UTC using RuboCop version 1.11.0.
+# on 2022-11-26 07:45:38 UTC using RuboCop version 1.22.3.
 # The point is for the user to remove these configuration records
 # one by one as the offenses are removed from the code base.
 # Note that changes in the inspected code, or installation of new
 # versions of RuboCop, may require this file to be generated again.
 
-# Offense count: 1
-# Cop supports --auto-correct.
-# Configuration parameters: TreatCommentsAsGroupSeparators, ConsiderPunctuation, Include.
-# Include: **/*.gemspec
-Gemspec/OrderedDependencies:
-  Exclude:
-    - 'paper_trail.gemspec'
-
-# Offense count: 5
-# Configuration parameters: IgnoredMethods, CountRepeatedAttributes.
-Metrics/AbcSize:
-  Max: 19 # Goal: the default (17?)
-
-# Offense count: 1
-# Configuration parameters: IgnoredMethods.
-Metrics/CyclomaticComplexity:
-  Max: 8 # Goal: 7
-
-# Offense count: 1
-# Configuration parameters: IgnoredMethods.
-Metrics/PerceivedComplexity:
-  Max: 9 # Goal: 7
-
-# Offense count: 1
-# Cop supports --auto-correct.
-Performance/BlockGivenWithExplicitBlock:
-  Exclude:
-    - 'lib/paper_trail.rb'
-
-# Offense count: 1
-# Configuration parameters: MinSize.
-Performance/CollectionLiteralInLoop:
-  Exclude:
-    - 'spec/models/version_spec.rb'
-
-# Offense count: 115
-# Configuration parameters: Prefixes.
-# Prefixes: when, with, without
-RSpec/ContextWording:
-  Enabled: false
-
 # It may be possible for us to use safe_load, but we'd have to pass the
 # safelists, like `whitelist_classes` into our serializer, and the serializer
 # interface is a public API, so that would be a breaking change.
+# Offense count: 13
+# Cop supports --auto-correct.
 Security/YAMLLoad:
   Exclude:
     - 'lib/paper_trail/serializers/yaml.rb'
+    - 'spec/models/book_spec.rb'
     - 'spec/models/gadget_spec.rb'
     - 'spec/models/no_object_spec.rb'
     - 'spec/models/person_spec.rb'
     - 'spec/models/version_spec.rb'
     - 'spec/paper_trail/events/destroy_spec.rb'
-    - 'spec/paper_trail/model_spec.rb'
-    - 'spec/paper_trail/serializer_spec.rb'
-
-# Offense count: 1
-# Cop supports --auto-correct.
-Rails/ApplicationController:
-  Exclude:
-    - 'spec/dummy_app/app/controllers/test_controller.rb'
-
-# Offense count: 56
-# Cop supports --auto-correct.
-Rails/ApplicationRecord:
-  Enabled: false
-
-# Offense count: 1
-# Cop supports --auto-correct.
-Rails/NegateInclude:
-  Exclude:
-    - 'lib/paper_trail/events/base.rb'
-
-# Offense count: 1
-# Cop supports --auto-correct.
-Rails/Presence:
-  Exclude:
-    - 'lib/paper_trail/reifier.rb'
-
-# Offense count: 2
-# Cop supports --auto-correct.
-# Configuration parameters: Include.
-# Include: **/Rakefile, **/*.rake
-Rails/RakeEnvironment:
-  Exclude:
-    - 'lib/capistrano/tasks/**/*.rake'
-    - 'Rakefile'
-
-# Offense count: 8
-# Cop supports --auto-correct.
-Rails/RedundantForeignKey:
-  Exclude:
-    - 'spec/dummy_app/app/models/family/family.rb'
-    - 'spec/dummy_app/app/models/family/family_line.rb'
-    - 'spec/dummy_app/app/models/person.rb'
-
-# Offense count: 28
-# Configuration parameters: ForbiddenMethods, AllowedMethods.
-# ForbiddenMethods: decrement!, decrement_counter, increment!, increment_counter, insert, insert!, insert_all, insert_all!, toggle!, touch, touch_all, update_all, update_attribute, update_column, update_columns, update_counters, upsert, upsert_all
-Rails/SkipsModelValidations:
-  Exclude:
-    - 'lib/paper_trail/record_trail.rb'
-    - 'spec/models/gadget_spec.rb'
-    - 'spec/models/on/create_spec.rb'
-    - 'spec/models/on/empty_array_spec.rb'
-    - 'spec/models/on/touch_spec.rb'
-    - 'spec/models/on/update_spec.rb'
-    - 'spec/models/widget_spec.rb'
-    - 'spec/paper_trail/cleaner_spec.rb'
-    - 'spec/paper_trail/config_spec.rb'
-    - 'spec/paper_trail/model_spec.rb'
-
-# Offense count: 1
-# Cop supports --auto-correct.
-Rails/WhereNot:
-  Exclude:
-    - 'lib/paper_trail/version_concern.rb'
-
-RSpec/FilePath:
-  Exclude:
-    - 'spec/paper_trail/model_spec.rb'
     - 'spec/paper_trail/serializer_spec.rb'
diff --git a/Appraisals b/Appraisals
index b4051e7..13a6df1 100644
--- a/Appraisals
+++ b/Appraisals
@@ -8,12 +8,6 @@
 # > appraisal. If something is specified in both the Gemfile and an appraisal,
 # > the version from the appraisal takes precedence.
 # > https://github.com/thoughtbot/appraisal
-#
-#
-appraise "rails-5.2" do
-  gem "rails", "~> 5.2.4"
-  gem "rails-controller-testing", "~> 1.0.2"
-end
 
 appraise "rails-6.0" do
   gem "rails", "~> 6.0.3"
@@ -24,3 +18,8 @@ appraise "rails-6.1" do
   gem "rails", "~> 6.1.0"
   gem "rails-controller-testing", "~> 1.0.5"
 end
+
+appraise "rails-7.0" do
+  gem "rails", "~> 7.0.3.1"
+  gem "rails-controller-testing", "~> 1.0.5"
+end
diff --git a/CHANGELOG.md b/CHANGELOG.md
index 7fad120..f29d57c 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -17,10 +17,142 @@ recommendations of [keepachangelog.com](http://keepachangelog.com/).
 
 - None
 
+## 14.0.0 (2022-11-26)
+
+### Breaking Changes
+
+- [#1399](https://github.com/paper-trail-gem/paper_trail/pull/1399) - Same
+  change re: `YAML.safe_load` as in 13.0.0, but this time for Rails 6.0 and 6.1.
+- [#1406](https://github.com/paper-trail-gem/paper_trail/pull/1406) -
+  Certain [Metadata][1] keys are now forbidden, like `id`, and `item_type`.
+  These keys are reserved by PT.
+
+### Dependencies
+
+- Drop support for Rails 5.2, which reached EoL on 2022-06-01
+- Drop support for Ruby 2.6, which reached EoL on 2022-03-31
+- Drop support for request_store < 1.4
+
+### Added
+
+- None
+
+### Fixed
+
+- [#1395](https://github.com/paper-trail-gem/paper_trail/issues/1395) -
+  Fix incorrect `Version#created_at` value when using
+  `PaperTrail::RecordTrail#update_columns`
+- [#1404](https://github.com/paper-trail-gem/paper_trail/pull/1404) -
+  Delay referencing ActiveRecord until after Railtie is loaded
+- Where possible, methods which are not part of PaperTrail's public API have
+  had their access changed to private. All of these methods had been clearly
+  marked as `@api private` in the documentation, for years. This is not expected
+  to be a breaking change.
+
+## 13.0.0 (2022-08-15)
+
+### Breaking Changes
+
+- For Rails >= 7.0, the default serializer will now use `YAML.safe_load` unless
+  `ActiveRecord.use_yaml_unsafe_load`. This change only affects users whose
+  `versions` table has `object` or `object_changes` columns of type `text`, and
+  who use the YAML serializer. People who use the JSON serializer, or those with
+  `json(b)` columns, are unaffected. Please see
+  [doc/pt_13_yaml_safe_load.md](doc/pt_13_yaml_safe_load.md) for details.
+
+### Added
+
+- None
+
+### Fixed
+
+- None
+
+## 12.3.0 (2022-03-13)
+
+### Breaking Changes
+
+- None
+
+### Added
+
+- [#1371](https://github.com/paper-trail-gem/paper_trail/pull/1371) - Added
+  `in_after_callback` argument to `PaperTrail::RecordTrail#save_with_version`,
+  to allow the caller to indicate if this method is being called during an
+  `after` callback. Defaults to `false`.
+- [#1374](https://github.com/paper-trail-gem/paper_trail/pull/1374) - Added
+  option `--uuid` when generating new migration. This can be used to set the
+  type of item_id column to uuid for use with paper_trail on a database that
+  uses uuid as primary key.
+
+### Fixed
+
+- [#1373](https://github.com/paper-trail-gem/paper_trail/issues/1373) - Add
+  CLI option to use uuid type for item_id when generating migration.
+- [#1376](https://github.com/paper-trail-gem/paper_trail/pull/1376) - Create a
+  version record when associated object is touched. Restores the behavior of
+  PaperTrail < v12.1.0.
+
+## 12.2.0 (2022-01-21)
+
+### Breaking Changes
+
+- None
+
+### Added
+
+- [#1365](https://github.com/paper-trail-gem/paper_trail/pull/1365) -
+  Support Rails 7.0
+- [#1349](https://github.com/paper-trail-gem/paper_trail/pull/1349) -
+  `if:` and `unless:` work with `touch` events now.
+
+### Fixed
+
+- [#1366](https://github.com/paper-trail-gem/paper_trail/pull/1366) -
+  Fixed a bug where the `create_versions` migration lead to a broken `db/schema.rb` for Ruby 3
+
+### Dependencies
+
+- [#1338](https://github.com/paper-trail-gem/paper_trail/pull/1338) -
+  Support Psych version 4
+- ruby >= 2.6 (was >= 2.5). Ruby 2.5 reached EoL on 2021-03-31.
+
+## 12.1.0 (2021-08-30)
+
+### Breaking Changes
+
+- None
+
+### Added
+
+- [#1292](https://github.com/paper-trail-gem/paper_trail/pull/1292) -
+  `where_attribute_changes` queries for versions where the object's attribute
+  changed to or from any values.
+- [#1291](https://github.com/paper-trail-gem/paper_trail/pull/1291) -
+  `where_object_changes_to` queries for versions where the object's attributes
+  changed to one set of known values from any other set of values.
+
+### Fixed
+
+- [#1285](https://github.com/paper-trail-gem/paper_trail/pull/1285) -
+  For ActiveRecord >= 6.0, the `touch` callback will no longer create a new
+  `Version` for skipped or ignored attributes.
+- [#1309](https://github.com/paper-trail-gem/paper_trail/pull/1309) -
+  Removes `item_subtype` requirement when specifying model-specific limits.
+- [#1333](https://github.com/paper-trail-gem/paper_trail/pull/1333) -
+  Improve reification of STI models that use `find_sti_class`/`sti_class_for`
+  to customize single table inheritance.
+
 ## 12.0.0 (2021-03-29)
 
 ### Breaking Changes
 
+- [#1281](https://github.com/paper-trail-gem/paper_trail/pull/1281) Rails:
+  Instead of an `Engine`, PT now provides a `Railtie`, which is simpler.
+  This was not expected to be a breaking change, but has caused trouble for
+  some people:
+  - Issue with the deprecated `autoloader = :classic` setting
+    (https://github.com/paper-trail-gem/paper_trail/issues/1305)
 - Rails: The deprecated `config.paper_trail` configuration technique
   has been removed. This configuration object was deprecated in 10.2.0. It only
   had one key, `config.paper_trail.enabled`. Please review docs section [2.d.
@@ -1272,3 +1404,5 @@ in the `PaperTrail::Version` class through a `Rails::Engine` when the gem is use
   - [#160](https://github.com/paper-trail-gem/paper_trail/pull/160) - Fixed failing tests and resolved out of date dependency issues.
   - [#157](https://github.com/paper-trail-gem/paper_trail/pull/157) - Refactored `class_attribute` names on the `ClassMethods` module
     for names that are not obviously pertaining to PaperTrail to prevent method name collision.
+
+[1]: https://github.com/paper-trail-gem/paper_trail#4c-storing-metadata
diff --git a/README.md b/README.md
index bc2f411..22f6947 100644
--- a/README.md
+++ b/README.md
@@ -10,21 +10,25 @@ has been destroyed.
 
 ## Documentation
 
-| Version        | Documentation |
-| -------------- | ------------- |
-| Unreleased     | https://github.com/paper-trail-gem/paper_trail/blob/master/README.md |
-| 12.0.0         | https://github.com/paper-trail-gem/paper_trail/blob/v12.0.0/README.md |
-| 11.1.0         | https://github.com/paper-trail-gem/paper_trail/blob/v11.1.0/README.md |
-| 10.3.1         | https://github.com/paper-trail-gem/paper_trail/blob/v10.3.1/README.md |
-| 9.2.0          | https://github.com/paper-trail-gem/paper_trail/blob/v9.2.0/README.md |
-| 8.1.2          | https://github.com/paper-trail-gem/paper_trail/blob/v8.1.2/README.md |
-| 7.1.3          | https://github.com/paper-trail-gem/paper_trail/blob/v7.1.3/README.md |
-| 6.0.2          | https://github.com/paper-trail-gem/paper_trail/blob/v6.0.2/README.md |
-| 5.2.3          | https://github.com/paper-trail-gem/paper_trail/blob/v5.2.3/README.md |
-| 4.2.0          | https://github.com/paper-trail-gem/paper_trail/blob/v4.2.0/README.md |
-| 3.0.9          | https://github.com/paper-trail-gem/paper_trail/blob/v3.0.9/README.md |
-| 2.7.2          | https://github.com/paper-trail-gem/paper_trail/blob/v2.7.2/README.md |
-| 1.6.5          | https://github.com/paper-trail-gem/paper_trail/blob/v1.6.5/README.md |
+This is the _user guide_. See also, the
+[API reference](https://www.rubydoc.info/gems/paper_trail).
+
+Choose version:
+[Unreleased](https://github.com/paper-trail-gem/paper_trail/blob/master/README.md),
+[14.0](https://github.com/paper-trail-gem/paper_trail/blob/v14.0.0/README.md),
+[13.0](https://github.com/paper-trail-gem/paper_trail/blob/v13.0.0/README.md),
+[12.3](https://github.com/paper-trail-gem/paper_trail/blob/v12.3.0/README.md),
+[11.1](https://github.com/paper-trail-gem/paper_trail/blob/v11.1.0/README.md),
+[10.3](https://github.com/paper-trail-gem/paper_trail/blob/v10.3.1/README.md),
+[9.2](https://github.com/paper-trail-gem/paper_trail/blob/v9.2.0/README.md),
+[8.1](https://github.com/paper-trail-gem/paper_trail/blob/v8.1.2/README.md),
+[7.1](https://github.com/paper-trail-gem/paper_trail/blob/v7.1.3/README.md),
+[6.0](https://github.com/paper-trail-gem/paper_trail/blob/v6.0.2/README.md),
+[5.2](https://github.com/paper-trail-gem/paper_trail/blob/v5.2.3/README.md),
+[4.2](https://github.com/paper-trail-gem/paper_trail/blob/v4.2.0/README.md),
+[3.0](https://github.com/paper-trail-gem/paper_trail/blob/v3.0.9/README.md),
+[2.7](https://github.com/paper-trail-gem/paper_trail/blob/v2.7.2/README.md),
+[1.6](https://github.com/paper-trail-gem/paper_trail/blob/v1.6.5/README.md)
 
 ## Table of Contents
 
@@ -47,6 +51,8 @@ has been destroyed.
   - [3.b. Navigating Versions](#3b-navigating-versions)
   - [3.c. Diffing Versions](#3c-diffing-versions)
   - [3.d. Deleting Old Versions](#3d-deleting-old-versions)
+  - [3.e. Queries](#3e-queries)
+  - [3.f. Defunct `item_id`s](#3f-defunct-item_ids)
 - [4. Saving More Information About Versions](#4-saving-more-information-about-versions)
   - [4.a. Finding Out Who Was Responsible For A Change](#4a-finding-out-who-was-responsible-for-a-change)
   - [4.b. Associations](#4b-associations)
@@ -60,6 +66,7 @@ has been destroyed.
   - [6.a. Custom Version Classes](#6a-custom-version-classes)
   - [6.b. Custom Serializer](#6b-custom-serializer)
   - [6.c. Custom Object Changes](#6c-custom-object-changes)
+  - [6.d. Excluding the Object Column](#6d-excluding-the-object-column)
 - [7. Testing](#7-testing)
   - [7.a. Minitest](#7a-minitest)
   - [7.b. RSpec](#7b-rspec)
@@ -82,28 +89,30 @@ has been destroyed.
 
 ### 1.a. Compatibility
 
-| paper_trail    | branch     | ruby     | activerecord  |
-| -------------- | ---------- | -------- | ------------- |
-| unreleased     | master     | >= 2.5.0 | >= 5.2, < 6.2 |
-| 12             | master     | >= 2.5.0 | >= 5.2, < 6.2 |
-| 11             | master     | >= 2.4.0 | >= 5.2, < 6.1 |
-| 10             | 10-stable  | >= 2.3.0 | >= 4.2, < 6.1 |
-| 9              | 9-stable   | >= 2.3.0 | >= 4.2, < 5.3 |
-| 8              | 8-stable   | >= 2.2.0 | >= 4.2, < 5.2 |
-| 7              | 7-stable   | >= 2.1.0 | >= 4.0, < 5.2 |
-| 6              | 6-stable   | >= 1.9.3 | >= 4.0, < 5.2 |
-| 5              | 5-stable   | >= 1.9.3 | >= 3.0, < 5.1 |
-| 4              | 4-stable   | >= 1.8.7 | >= 3.0, < 5.1 |
-| 3              | 3.0-stable | >= 1.8.7 | >= 3.0, < 5   |
-| 2              | 2.7-stable | >= 1.8.7 | >= 3.0, < 4   |
-| 1              | rails2     | >= 1.8.7 | >= 2.3, < 3   |
+| paper_trail | branch     | ruby     | activerecord  |
+|-------------|------------|----------|---------------|
+| unreleased  | master     | >= 2.7.0 | >= 6.0, < 7.1 |
+| 14          | 14-stable  | >= 2.7.0 | >= 6.0, < 7.1 |
+| 13          | 13-stable  | >= 2.6.0 | >= 5.2, < 7.1 |
+| 12          | 12-stable  | >= 2.6.0 | >= 5.2, < 7.1 |
+| 11          | 11-stable  | >= 2.4.0 | >= 5.2, < 6.1 |
+| 10          | 10-stable  | >= 2.3.0 | >= 4.2, < 6.1 |
+| 9           | 9-stable   | >= 2.3.0 | >= 4.2, < 5.3 |
+| 8           | 8-stable   | >= 2.2.0 | >= 4.2, < 5.2 |
+| 7           | 7-stable   | >= 2.1.0 | >= 4.0, < 5.2 |
+| 6           | 6-stable   | >= 1.9.3 | >= 4.0, < 5.2 |
+| 5           | 5-stable   | >= 1.9.3 | >= 3.0, < 5.1 |
+| 4           | 4-stable   | >= 1.8.7 | >= 3.0, < 5.1 |
+| 3           | 3.0-stable | >= 1.8.7 | >= 3.0, < 5   |
+| 2           | 2.7-stable | >= 1.8.7 | >= 3.0, < 4   |
+| 1           | rails2     | >= 1.8.7 | >= 2.3, < 3   |
 
 Experts: to install incompatible versions of activerecord, see
 `paper_trail/compatibility.rb`.
 
 ### 1.b. Installation
 
-1. Add PaperTrail to your `Gemfile`.
+1. Add PaperTrail to your `Gemfile` and run [`bundle`][57].
 
     `gem 'paper_trail'`
 
@@ -112,16 +121,13 @@ Experts: to install incompatible versions of activerecord, see
     ```
     bundle exec rails generate paper_trail:install [--with-changes]
     ```
+    
+    If tables in your project use `uuid` instead of `integers` for `id`, then use:  
+    ```
+    bundle exec rails generate paper_trail:install [--uuid]
+    ```
 
-    For more information on this generator, see [section 5.c.
-    Generators](#5c-generators).
-
-    If using [rails_admin][38], you must enable the
-    experimental [Associations](#4b-associations) feature.
-
-    If you're getting "Could not find generator 'paper_trail:install'" errors from
-    recent Ruby/Rails versions, try running `spring stop`
-    (see [this thread](https://github.com/paper-trail-gem/paper_trail/issues/459) for more details).
+    See [section 5.c. Generators](#5c-generators) for details.
 
     ```
     bundle exec rake db:migrate
@@ -160,7 +166,7 @@ Once you have a version, you can find out what happened:
 
 ```ruby
 v = widget.versions.last
-v.event # 'update', 'create', 'destroy'. See also: Custom Event Names
+v.event # 'update', 'create', 'destroy'. See also: "The versions.event Column"
 v.created_at
 v.whodunnit # ID of `current_user`. Requires `set_paper_trail_whodunnit` callback.
 widget = v.reify # The widget as it was before the update (nil for a create event)
@@ -374,8 +380,8 @@ end
 ```
 
 The `paper_trail.on_destroy` method can be further configured to happen
-`:before` or `:after` the destroy event. In PaperTrail 4, the default is
-`:after`. In PaperTrail 5, the default will be `:before`, to support
+`:before` or `:after` the destroy event. Until PaperTrail 4, the default was
+`:after`. Starting with PaperTrail 5, the default is `:before`, to support
 ActiveRecord 5. (see https://github.com/paper-trail-gem/paper_trail/pull/683)
 
 ### 2.b. Choosing When To Save New Versions
@@ -411,7 +417,7 @@ my_model.paper_trail.save_with_version
 
 #### Ignore
 
-You can `ignore` changes to certain attributes:
+If you don't want a version created when only a certain attribute changes, you can `ignore` that attribute:
 
 ```ruby
 class Article < ActiveRecord::Base
@@ -432,6 +438,8 @@ a.versions.length                         # 2
 a.paper_trail.previous_version.title      # 'My Title'
 ```
 
+Note: ignored fields will be stored in the version records. If you want to keep a field out of the versions table, use [`:skip`](#skip) instead of `:ignore`; skipped fields are also implicitly ignored.
+
 The `:ignore` option can also accept `Hash` arguments that we are considering deprecating.
 
 ```ruby
@@ -495,17 +503,31 @@ article being saved if a changed attribute is included in `:only` but not in
 
 #### Skip
 
-You can skip attributes completely with the `:skip` option.  As with `:ignore`,
+If you never want a field's values in the versions table, you can `:skip` the attribute.  As with `:ignore`,
 updates to these attributes will not create a version record.  In addition, if a
 version record is created for some other reason, these attributes will not be
 persisted.
 
 ```ruby
-class Article < ActiveRecord::Base
-  has_paper_trail skip: [:file_upload]
+class Author < ActiveRecord::Base
+  has_paper_trail skip: [:social_security_number]
 end
 ```
 
+Author's social security numbers will never appear in the versions log, and if an author updates only their social security number, it won't create a version record.
+
+#### Comparing `:ignore`, `:only`, and `:skip`
+
+- `:only` is basically the same as `:ignore`, but its inverse.
+- `:ignore` controls whether paper_trail will create a version record or not.
+- `:skip` controls whether paper_trail will save that field with the version record.
+- Skipped fields are also implicitly ignored. paper_trail does this internally.
+- Ignored fields are not implicitly skipped.
+
+So:
+- Ignore a field if you don't want a version record created when it's the only field to change.
+- Skip a field if you don't want it to be saved with any version records.
+
 ### 2.d. Turning PaperTrail Off
 
 PaperTrail is on by default, but sometimes you don't want to record versions.
@@ -616,10 +638,6 @@ has_paper_trail limit: 2
 has_paper_trail limit: nil
 ```
 
-To use a per-model limit, your `versions` table must have an
-`item_subtype` column. See [Section
-4.b.1](https://github.com/paper-trail-gem/paper_trail#4b1-the-optional-item_subtype-column).
-
 ## 3. Working With Versions
 
 ### 3.a. Reverting And Undeleting A Model
@@ -710,34 +728,20 @@ widget = widget.paper_trail.previous_version
 widget.paper_trail.live?            # false
 ```
 
-And you can perform `WHERE` queries for object versions based on attributes:
-
-```ruby
-# Find versions that meet these criteria.
-PaperTrail::Version.where_object(content: 'Hello', title: 'Article')
-
-# Find versions before and after attribute `atr` had value `v`:
-PaperTrail::Version.where_object_changes(atr: 'v')
-```
-
-Using `where_object_changes` to read YAML from a text column was deprecated in
-8.1.0, and will now raise an error.
+See also: Section 3.e. Queries
 
 ### 3.c. Diffing Versions
 
 There are two scenarios: diffing adjacent versions and diffing non-adjacent
 versions.
 
-The best way to diff adjacent versions is to get PaperTrail to do it for you.
-If you add an `object_changes` text column to your `versions` table, either at
-installation time with the `rails generate paper_trail:install --with-changes`
-option or manually, PaperTrail will store the `changes` diff (excluding any
-attributes PaperTrail is ignoring) in each `update` version.  You can use the
-`version.changeset` method to retrieve it.  For example:
+The best way to diff adjacent versions is to get PaperTrail to do it for you. If
+you add an `object_changes` column to your `versions` table, PaperTrail will
+store the `changes` diff in each version. Ignored attributes are omitted.
 
 ```ruby
 widget = Widget.create name: 'Bob'
-widget.versions.last.changeset
+widget.versions.last.changeset # reads object_changes column
 # {
 #   "name"=>[nil, "Bob"],
 #   "created_at"=>[nil, 2015-08-10 04:10:40 UTC],
@@ -758,11 +762,12 @@ widget.versions.last.changeset
 Prior to 10.0.0, the `object_changes` were only stored for create and update
 events. As of 10.0.0, they are stored for all three events.
 
-Please be aware that PaperTrail doesn't use diffs internally.  When I designed
-PaperTrail I wanted simplicity and robustness so I decided to make each version
-of an object self-contained.  A version stores all of its object's data, not a
-diff from the previous version.  This means you can delete any version without
-affecting any other.
+PaperTrail doesn't use diffs internally.
+
+> When I designed PaperTrail I wanted simplicity and robustness so I decided to
+> make each version of an object self-contained.  A version stores all of its
+> object's data, not a diff from the previous version.  This means you can
+> delete any version without affecting any other. -Andy
 
 To diff non-adjacent versions you'll have to write your own code.  These
 libraries may help:
@@ -777,12 +782,7 @@ For diffing two strings:
   or arbitrary-boundary-string-wise diffs.  Works very well on non-HTML input.
 * [diff-lcs][21]: old-school, line-wise diffs.
 
-For diffing two ActiveRecord objects:
-
-* [Jeremy Weiskotten's PaperTrail fork][22]: uses ActiveSupport's diff to return
-  an array of hashes of the changes.
-* [activerecord-diff][23]: rather like ActiveRecord::Dirty but also allows you
-  to specify which columns to compare.
+Unfortunately, there is no currently widely available and supported library for diffing two ActiveRecord objects.
 
 ### 3.d. Deleting Old Versions
 
@@ -798,6 +798,57 @@ sql> delete from versions where created_at < '2010-06-01';
 PaperTrail::Version.where('created_at < ?', 1.day.ago).delete_all
 ```
 
+### 3.e. Queries
+
+You can query records in the `versions` table based on their `object` or
+`object_changes` columns.
+
+```ruby
+# Find versions that meet these criteria.
+PaperTrail::Version.where_object(content: 'Hello', title: 'Article')
+
+# Find versions before and after attribute `atr` had value `v`:
+PaperTrail::Version.where_object_changes(atr: 'v')
+```
+
+See also:
+
+- `where_object_changes_from`
+- `where_object_changes_to`
+- `where_attribute_changes`
+
+Only `where_object` supports text columns. Your `object_changes` column should
+be a `json` or `jsonb` column if possible. If you must use a `text` column,
+you'll have to write a [custom
+`object_changes_adapter`](#6c-custom-object-changes).
+
+### 3.f. Defunct `item_id`s
+
+The `item_id`s in your `versions` table can become defunct over time,
+potentially causing application errors when `id`s in the foreign table are
+reused. `id` reuse can be an explicit choice of the application, or implicitly
+caused by sequence cycling. The chance of `id` reuse is reduced (but not
+eliminated) with `bigint` `id`s or `uuid`s, `no cycle`
+[sequences](https://www.postgresql.org/docs/current/sql-createsequence.html),
+and/or when `versions` are periodically deleted.
+
+Ideally, a Foreign Key Constraint (FKC) would set `item_id` to null when an item
+is deleted. However, `items` is a polymorphic relationship. A partial FKC (e.g.
+an FKC with a `where` clause) is possible, but only in Postgres, and it is
+impractical to maintain FKCs for every versioned table unless the number of
+such tables is very small.
+
+If [per-table `Version`
+classes](https://github.com/paper-trail-gem/paper_trail#6a-custom-version-classes)
+are used, then a partial FKC is no longer needed. So, a normal FKC can be
+written in any RDBMS, but it remains impractical to maintain so many FKCs.
+
+Some applications choose to handle this problem by "soft-deleting" versioned
+records, i.e. marking them as deleted instead of actually deleting them. This
+completely prevents `id` reuse, but adds complexity to the application. In most
+applications, this is the only known practical solution to the `id` reuse
+problem.
+
 ## 4. Saving More Information About Versions
 
 ### 4.a. Finding Out Who Was Responsible For A Change
@@ -991,13 +1042,19 @@ PaperTrail::Version.where(author_id: author_id)
 inserted into its own columns.
 
 | *PT Column*    | *How bad of an idea?* | *Alternative*                 |
-| -------------- | --------------------- | ----------------------------- |
-| item_type      | terrible idea         |                               |
-| item_id        | terrible idea         |                               |
+|----------------|-----------------------|-------------------------------|
+| created_at     | forbidden*            |                               |
 | event          | meh                   | paper_trail_event             |
-| whodunnit      | meh                   | PaperTrail.request.whodunnit= |
+| id             | forbidden             |                               |
+| item_id        | forbidden             |                               |
+| item_subtype   | forbidden             |                               |
+| item_type      | forbidden             |                               |
 | object         | a little dangerous    |                               |
 | object_changes | a little dangerous    |                               |
+| updated_at     | forbidden             |                               |
+| whodunnit      | meh                   | PaperTrail.request.whodunnit= |
+
+\* forbidden - raises a `PaperTrail::InvalidOption` error as of PT 14
 
 ## 5. ActiveRecord
 
@@ -1095,10 +1152,12 @@ Be advised that redefining an association is an undocumented feature of Rails.
 ### 5.c. Generators
 
 PaperTrail has one generator, `paper_trail:install`. It writes, but does not
-run, a migration file.
-The migration adds (at least) the `versions` table. The
-most up-to-date documentation for this generator can be found by running `rails
-generate paper_trail:install --help`, but a copy is included here for
+run, a migration file. The migration creates the `versions` table.
+
+#### Reference
+
+The most up-to-date documentation for this generator can be found by running
+`rails generate paper_trail:install --help`, but a copy is included here for
 convenience.
 
 ```
@@ -1107,6 +1166,7 @@ Usage:
 
 Options:
   [--with-changes], [--no-with-changes]            # Store changeset (diff) with each version
+  [--uuid]                                         # To use paper_trail with projects using uuid for id
 
 Runtime options:
   -f, [--force]                    # Overwrite files that already exist
@@ -1180,17 +1240,20 @@ class PostVersion < PaperTrail::Version
 end
 ```
 
-If you only use custom version classes and don't have a `versions` table, you
-must let ActiveRecord know that the `PaperTrail::Version` class is an
-`abstract_class`.
+If you only use custom version classes and don't have a `versions` table, you must
+let ActiveRecord know that your base version class (eg. `ApplicationVersion` below)
+class is an `abstract_class`.
 
 ```ruby
-# app/models/paper_trail/version.rb
-module PaperTrail
-  class Version < ActiveRecord::Base
-    include PaperTrail::VersionConcern
-    self.abstract_class = true
-  end
+# app/models/application_version.rb
+class ApplicationVersion < ActiveRecord::Base
+  include PaperTrail::VersionConcern
+  self.abstract_class = true
+end
+
+class PostVersion < ApplicationVersion
+  self.table_name = :post_versions
+  self.sequence_name = :post_versions_id_seq
 end
 ```
 
@@ -1363,15 +1426,24 @@ reading `::PaperTrail::Events::Base#recordable_object_changes`.
 
 An adapter can implement any or all of the following methods:
 
-1. diff: Returns the changeset in the desired format given the changeset in the original format
+1. diff: Returns the changeset in the desired format given the changeset in the
+  original format
 2. load_changeset: Returns the changeset for a given version object
-3. where_object_changes: Returns the records resulting from the given hash of attributes.
-4. where_object_changes_from: Returns the records resulting from the given hash of attributes where the attributes changed *from* the provided value(s).
+3. where_object_changes: Returns the records resulting from the given hash of
+  attributes.
+4. where_object_changes_from: Returns the records resulting from the given hash
+  of attributes where the attributes changed *from* the provided value(s).
+5. where_object_changes_to: Returns the records resulting from the given hash of
+  attributes where the attributes changed *to* the provided value(s).
+6. where_attribute_changes: Returns the records where the attribute changed to
+  or from any value.
+
+Depending on your needs, you may choose to implement only a subset of these
+methods.
 
-Depending on what your adapter does, you may have to implement all three.
+#### Known Adapters
 
-For an example of a complete and useful adapter, see
-[paper_trail-hashdiff](https://github.com/hashwin/paper_trail-hashdiff)
+- [paper_trail-hashdiff](https://github.com/hashwin/paper_trail-hashdiff)
 
 ### 6.d. Excluding the Object Column
 
@@ -1607,8 +1679,24 @@ require 'paper_trail/frameworks/rspec'
 ```
 
 ## 8. PaperTrail Plugins
+
+- paper_trail-active_record
 - [paper_trail-association_tracking][6] - track and reify associations
+- paper_trail-audit
+- paper_trail-background
 - [paper_trail-globalid][49] - enhances whodunnit by adding an `actor`
+- paper_trail-hashdiff
+- paper_trail-rails
+- paper_trail-related_changes
+- paper_trail-sinatra
+- paper_trail_actor
+- paper_trail_changes
+- paper_trail_manager
+- paper_trail_scrapbook
+- paper_trail_ui
+- revertible_paper_trail
+- rspec-paper_trail
+- sequel_paper_trail
 
 ## 9. Integration with Other Libraries
 
@@ -1683,8 +1771,6 @@ Released under the MIT licence.
 [19]: http://github.com/myobie/htmldiff
 [20]: http://github.com/pvande/differ
 [21]: https://github.com/halostatue/diff-lcs
-[22]: http://github.com/jeremyw/paper_trail/blob/master/lib/paper_trail/has_paper_trail.rb#L151-156
-[23]: http://github.com/tim/activerecord-diff
 [24]: https://github.com/paper-trail-gem/paper_trail/blob/master/lib/paper_trail/serializers/yaml.rb
 [25]: https://github.com/paper-trail-gem/paper_trail/blob/master/lib/paper_trail/serializers/json.rb
 [26]: http://www.postgresql.org/docs/9.4/static/datatype-json.html
@@ -1718,3 +1804,4 @@ Released under the MIT licence.
 [54]: https://rubygems.org/gems/paper_trail
 [55]: https://api.dependabot.com/badges/compatibility_score?dependency-name=paper_trail&package-manager=bundler&version-scheme=semver
 [56]: https://dependabot.com/compatibility-score.html?dependency-name=paper_trail&package-manager=bundler&version-scheme=semver
+[57]: https://bundler.io/v2.3/man/bundle-install.1.html
diff --git a/Rakefile b/Rakefile
index 3f814c5..0c7ed0c 100644
--- a/Rakefile
+++ b/Rakefile
@@ -38,11 +38,8 @@ task :clean do
   end
 end
 
-desc <<~EOS
-  Write a database.yml for the specified RDBMS, and create database. Does not
-  migrate. Migration happens later in spec_helper.
-EOS
-task prepare: %i[clean install_database_yml] do
+desc "Create the database."
+task :create_db do
   puts format("creating %s database", ENV["DB"])
   case ENV["DB"]
   when "mysql"
@@ -59,6 +56,12 @@ task prepare: %i[clean install_database_yml] do
   end
 end
 
+desc <<~EOS
+  Write a database.yml for the specified RDBMS, and create database. Does not
+  migrate. Migration happens later in spec_helper.
+EOS
+task prepare: %i[clean install_database_yml create_db]
+
 require "rspec/core/rake_task"
 desc "Run tests on PaperTrail with RSpec"
 task(:spec).clear
diff --git a/debian/changelog b/debian/changelog
index dc9caa5..f4cb64b 100644
--- a/debian/changelog
+++ b/debian/changelog
@@ -1,3 +1,9 @@
+ruby-paper-trail (14.0.0-1) UNRELEASED; urgency=low
+
+  * New upstream release.
+
+ -- Debian Janitor <janitor@jelmer.uk>  Fri, 20 Jan 2023 06:31:51 -0000
+
 ruby-paper-trail (12.0.0-1) unstable; urgency=medium
 
   * Team upload.
diff --git a/doc/bug_report_template.rb b/doc/bug_report_template.rb
index 2f7d639..a093860 100644
--- a/doc/bug_report_template.rb
+++ b/doc/bug_report_template.rb
@@ -1,4 +1,4 @@
 # frozen_string_literal: true
 
-# Moved to .github/ISSUE_TEMPLATE/bug_report.md
+# Moved to .github/ISSUE_TEMPLATE/bug-report.md
 # Please update your bookmarks
diff --git a/doc/pt_13_yaml_safe_load.md b/doc/pt_13_yaml_safe_load.md
new file mode 100644
index 0000000..7d060ee
--- /dev/null
+++ b/doc/pt_13_yaml_safe_load.md
@@ -0,0 +1,53 @@
+# PT 13 uses YAML.safe_load
+
+Starting with 13.0.0, in Rails >= 7.0, PT's default serializer
+(`PaperTrail::Serializers::YAML`) will use `safe_load` unless
+`ActiveRecord.use_yaml_unsafe_load`.
+
+PT 14.0.0 extends this protection to Rails 6.
+
+Earlier versions of PT use `unsafe_load`.
+
+## Motivation
+
+> A few days ago Rails released versions 7.0.3.1, 6.1.6.1, 6.0.5.1, and 5.2.8.1.
+> These are security updates that impact applications that use serialised
+> attributes on Active Record models. These updates, identified by CVE-2022-32224
+> cover a possible escalation to RCE when using YAML serialised columns in Active
+> Record.
+> https://rubyonrails.org/2022/7/15/this-week-in-rails-rails-security-releases-improved-generator-option-handling-and-more-24774592
+
+## Who is affected by this change?
+
+This change only affects users whose `versions` table has `object` or
+`object_changes` columns of type `text`, and who use the YAML serializer. People
+who use the JSON serializer, or those with `json(b)` columns, are unaffected.
+
+## To continue using the YAML serializer
+
+We recommend switching to `json(b)` columns, or at least JSON in a `text` column
+(see "Other serializers" below). If you must continue using the YAML serializer,
+PT users are required to configure `ActiveRecord.yaml_column_permitted_classes`
+correctly for their own application. Users may want to start with the following
+safe-list:
+
+```ruby
+::ActiveRecord.use_yaml_unsafe_load = false
+::ActiveRecord.yaml_column_permitted_classes = [
+  ::ActiveRecord::Type::Time::Value,
+  ::ActiveSupport::TimeWithZone,
+  ::ActiveSupport::TimeZone,
+  ::BigDecimal,
+  ::Date,
+  ::Symbol,
+  ::Time
+]
+```
+
+## Other serializers
+
+While YAML remains the default serializer in PT for historical compatibility,
+we have recommended JSON instead, for years. See:
+
+- [PostgreSQL JSON column type support](https://github.com/paper-trail-gem/paper_trail/blob/v12.3.0/README.md#postgresql-json-column-type-support)
+- [Convert existing YAML data to JSON](https://github.com/paper-trail-gem/paper_trail/blob/v12.3.0/README.md#convert-existing-yaml-data-to-json)
diff --git a/doc/triage.md b/doc/triage.md
index 453d6e9..0a9d468 100644
--- a/doc/triage.md
+++ b/doc/triage.md
@@ -13,7 +13,7 @@ For instructions on how to file a bug report, please see our [issue template][3]
 
 [1]: https://github.com/paper-trail-gem/paper_trail/blob/master/.github/CONTRIBUTING.md
 [2]: https://stackoverflow.com/tags/paper-trail-gem
-[3]: https://github.com/paper-trail-gem/paper_trail/blob/master/.github/ISSUE_TEMPLATE/bug_report.md
+[3]: https://github.com/paper-trail-gem/paper_trail/blob/master/.github/ISSUE_TEMPLATE/bug-report.md
 ```
 
 ## Responses to Common Problems
@@ -24,7 +24,7 @@ Hi ___. All issues are required to use our [issue template][2]. See also our
 and I'll do what I can to help!
 
 [1]: https://github.com/paper-trail-gem/paper_trail/blob/master/.github/CONTRIBUTING.md
-[2]: https://github.com/paper-trail-gem/paper_trail/blob/master/.github/ISSUE_TEMPLATE/bug_report.md
+[2]: https://github.com/paper-trail-gem/paper_trail/blob/master/.github/ISSUE_TEMPLATE/bug-report.md
 ```
 
 ## Usage question masquerading as a feature proposal
diff --git a/doc/warning_about_not_setting_whodunnit.md b/doc/warning_about_not_setting_whodunnit.md
index 4cdab97..1bf5058 100644
--- a/doc/warning_about_not_setting_whodunnit.md
+++ b/doc/warning_about_not_setting_whodunnit.md
@@ -9,7 +9,7 @@ After upgrading to PaperTrail 5, you see this warning:
 ## You want to track whodunnit
 
 Add `before_action :set_paper_trail_whodunnit` to your ApplicationController.
-See the PaperTrail readme for an example (https://git.io/vrsbt).
+See the PaperTrail readme for an example (https://github.com/paper-trail-gem/paper_trail#4a-finding-out-who-was-responsible-for-a-change).
 
 ## You don't want to track whodunnit
 
diff --git a/gemfiles/rails_5.2.gemfile b/gemfiles/rails_7.0.gemfile
similarity index 57%
rename from gemfiles/rails_5.2.gemfile
rename to gemfiles/rails_7.0.gemfile
index 4fd4d62..58bdd3c 100644
--- a/gemfiles/rails_5.2.gemfile
+++ b/gemfiles/rails_7.0.gemfile
@@ -2,7 +2,7 @@
 
 source "https://rubygems.org"
 
-gem "rails", "~> 5.2.4"
-gem "rails-controller-testing", "~> 1.0.2"
+gem "rails", "~> 7.0.3.1"
+gem "rails-controller-testing", "~> 1.0.5"
 
 gemspec path: "../"
diff --git a/lib/generators/paper_trail/install/install_generator.rb b/lib/generators/paper_trail/install/install_generator.rb
index 6fc82fe..1378663 100644
--- a/lib/generators/paper_trail/install/install_generator.rb
+++ b/lib/generators/paper_trail/install/install_generator.rb
@@ -20,6 +20,12 @@ module PaperTrail
       default: false,
       desc: "Store changeset (diff) with each version"
     )
+    class_option(
+      :uuid,
+      type: :boolean,
+      default: false,
+      desc: "Use uuid instead of bigint for item_id type (use only if tables use UUIDs)"
+    )
 
     desc "Generates (but does not run) a migration to add a versions table." \
          "  See section 5.c. Generators in README.md for more information."
@@ -28,7 +34,8 @@ module PaperTrail
       add_paper_trail_migration(
         "create_versions",
         item_type_options: item_type_options,
-        versions_table_options: versions_table_options
+        versions_table_options: versions_table_options,
+        item_id_type_options: item_id_type_options
       )
       if options.with_changes?
         add_paper_trail_migration("add_object_changes_to_versions")
@@ -37,13 +44,18 @@ module PaperTrail
 
     private
 
+    # To use uuid instead of integer for primary key
+    def item_id_type_options
+      options.uuid? ? "string" : "bigint"
+    end
+
     # MySQL 5.6 utf8mb4 limit is 191 chars for keys used in indexes.
     # See https://github.com/paper-trail-gem/paper_trail/issues/651
     def item_type_options
       if mysql?
-        ", { null: false, limit: 191 }"
+        ", null: false, limit: 191"
       else
-        ", { null: false }"
+        ", null: false"
       end
     end
 
diff --git a/lib/generators/paper_trail/install/templates/create_versions.rb.erb b/lib/generators/paper_trail/install/templates/create_versions.rb.erb
index 268d929..bc44f29 100644
--- a/lib/generators/paper_trail/install/templates/create_versions.rb.erb
+++ b/lib/generators/paper_trail/install/templates/create_versions.rb.erb
@@ -11,7 +11,7 @@ class CreateVersions < ActiveRecord::Migration<%= migration_version %>
   def change
     create_table :versions<%= versions_table_options %> do |t|
       t.string   :item_type<%= item_type_options %>
-      t.bigint   :item_id,   null: false
+      t.<%= item_id_type_options %>   :item_id,   null: false
       t.string   :event,     null: false
       t.string   :whodunnit
       t.text     :object, limit: TEXT_BYTES
@@ -29,8 +29,10 @@ class CreateVersions < ActiveRecord::Migration<%= migration_version %>
       # version of ActiveRecord with support for fractional seconds in MySQL.
       # (https://github.com/rails/rails/pull/14359)
       #
+      # MySQL users should use the following line for `created_at`
+      # t.datetime :created_at, limit: 6
       t.datetime :created_at
     end
-    add_index :versions, %i(item_type item_id)
+    add_index :versions, %i[item_type item_id]
   end
 end
diff --git a/lib/generators/paper_trail/update_item_subtype/update_item_subtype_generator.rb b/lib/generators/paper_trail/update_item_subtype/update_item_subtype_generator.rb
index f0b8464..9278dac 100644
--- a/lib/generators/paper_trail/update_item_subtype/update_item_subtype_generator.rb
+++ b/lib/generators/paper_trail/update_item_subtype/update_item_subtype_generator.rb
@@ -7,8 +7,10 @@ module PaperTrail
   class UpdateItemSubtypeGenerator < MigrationGenerator
     source_root File.expand_path("templates", __dir__)
 
-    desc "Generates (but does not run) a migration to update item_subtype for STI entries in an "\
-      "existing versions table."
+    desc(
+      "Generates (but does not run) a migration to update item_subtype for "\
+      "STI entries in an existing versions table."
+    )
 
     def create_migration_file
       add_paper_trail_migration("update_versions_for_item_subtype", sti_type_options: options)
diff --git a/lib/paper_trail.rb b/lib/paper_trail.rb
index 51b895b..ba44450 100644
--- a/lib/paper_trail.rb
+++ b/lib/paper_trail.rb
@@ -8,6 +8,13 @@
 # can revisit this decision.
 require "active_support/all"
 
+# We used to `require "active_record"` here, but that was [replaced with a
+# Railtie](https://github.com/paper-trail-gem/paper_trail/pull/1281) in PT 12.
+# As a result, we cannot reference `ActiveRecord` in this file (ie. until our
+# Railtie has loaded). If we did, it would cause [problems with non-Rails
+# projects](https://github.com/paper-trail-gem/paper_trail/pull/1401).
+
+require "paper_trail/errors"
 require "paper_trail/cleaner"
 require "paper_trail/compatibility"
 require "paper_trail/config"
@@ -68,7 +75,7 @@ module PaperTrail
     #
     # @api public
     def request(options = nil, &block)
-      if options.nil? && !block_given?
+      if options.nil? && !block
         Request
       else
         Request.with(options, &block)
@@ -78,7 +85,7 @@ module PaperTrail
     # Set the field which records when a version was created.
     # @api public
     def timestamp_field=(_field_name)
-      raise(E_TIMESTAMP_FIELD_CONFIG)
+      raise Error, E_TIMESTAMP_FIELD_CONFIG
     end
 
     # Set the PaperTrail serializer. This setting affects all threads.
@@ -95,7 +102,7 @@ module PaperTrail
 
     # Returns PaperTrail's global configuration object, a singleton. These
     # settings affect all threads.
-    # @api private
+    # @api public
     def config
       @config ||= PaperTrail::Config.instance
       yield @config if block_given?
@@ -103,6 +110,7 @@ module PaperTrail
     end
     alias configure config
 
+    # @api public
     def version
       VERSION::STRING
     end
diff --git a/lib/paper_trail/attribute_serializers/cast_attribute_serializer.rb b/lib/paper_trail/attribute_serializers/cast_attribute_serializer.rb
index 5fcdb05..2882706 100644
--- a/lib/paper_trail/attribute_serializers/cast_attribute_serializer.rb
+++ b/lib/paper_trail/attribute_serializers/cast_attribute_serializer.rb
@@ -32,11 +32,21 @@ module PaperTrail
         if defined_enums[attr] && val.is_a?(::String)
           # Because PT 4 used to save the string version of enums to `object_changes`
           val
+        elsif rails_gte_7_0? && val.is_a?(ActiveRecord::Type::Time::Value)
+          # Because Rails 7 time attribute throws a delegation error when you deserialize
+          # it with the factory.
+          # See ActiveRecord::Type::Time::Value crashes when loaded from YAML on rails 7.0
+          # https://github.com/rails/rails/issues/43966
+          val.instance_variable_get(:@time)
         else
           AttributeSerializerFactory.for(@klass, attr).deserialize(val)
         end
       end
 
+      def rails_gte_7_0?
+        ::ActiveRecord.gem_version >= ::Gem::Version.new("7.0.0")
+      end
+
       def serialize(attr, val)
         AttributeSerializerFactory.for(@klass, attr).serialize(val)
       end
diff --git a/lib/paper_trail/compatibility.rb b/lib/paper_trail/compatibility.rb
index d27031d..a93e696 100644
--- a/lib/paper_trail/compatibility.rb
+++ b/lib/paper_trail/compatibility.rb
@@ -8,7 +8,7 @@ module PaperTrail
   #
   # It is not safe to assume that a new version of rails will be compatible with
   # PaperTrail. PT is only compatible with the versions of rails that it is
-  # tested against. See `.travis.yml`.
+  # tested against. See `.github/workflows/test.yml`.
   #
   # However, as of
   # [#1213](https://github.com/paper-trail-gem/paper_trail/pull/1213) our
@@ -17,8 +17,8 @@ module PaperTrail
   # newer rails versions. Most PT users should avoid incompatible rails
   # versions.
   module Compatibility
-    ACTIVERECORD_GTE = ">= 5.2" # enforced in gemspec
-    ACTIVERECORD_LT = "< 6.2" # not enforced in gemspec
+    ACTIVERECORD_GTE = ">= 6.0" # enforced in gemspec
+    ACTIVERECORD_LT = "< 7.1" # not enforced in gemspec
 
     E_INCOMPATIBLE_AR = <<-EOS
       PaperTrail %s is not compatible with ActiveRecord %s. We allow PT
diff --git a/lib/paper_trail/errors.rb b/lib/paper_trail/errors.rb
new file mode 100644
index 0000000..0d3f60a
--- /dev/null
+++ b/lib/paper_trail/errors.rb
@@ -0,0 +1,33 @@
+# frozen_string_literal: true
+
+module PaperTrail
+  # Generic PaperTrail exception.
+  # @api public
+  class Error < StandardError
+  end
+
+  # An unexpected option, perhaps a typo, was passed to a public API method.
+  # @api public
+  class InvalidOption < Error
+  end
+
+  # The application's database schema is not supported.
+  # @api public
+  class UnsupportedSchema < Error
+  end
+
+  # The application's database column type is not supported.
+  # @api public
+  class UnsupportedColumnType < UnsupportedSchema
+    def initialize(method:, expected:, actual:)
+      super(
+        format(
+          "%s expected %s column, got %s",
+          method,
+          expected,
+          actual
+        )
+      )
+    end
+  end
+end
diff --git a/lib/paper_trail/events/base.rb b/lib/paper_trail/events/base.rb
index 9a1a226..f84a8b4 100644
--- a/lib/paper_trail/events/base.rb
+++ b/lib/paper_trail/events/base.rb
@@ -22,6 +22,19 @@ module PaperTrail
     #
     # @api private
     class Base
+      E_FORBIDDEN_METADATA_KEY = <<-EOS.squish
+        Forbidden metadata key: %s. As of PT 14, the following metadata keys are
+        forbidden: %s
+      EOS
+      FORBIDDEN_METADATA_KEYS = %i[
+        created_at
+        id
+        item_id
+        item_subtype
+        item_type
+        updated_at
+      ].freeze
+
       # @api private
       def initialize(record, in_after_callback)
         @record = record
@@ -44,6 +57,13 @@ module PaperTrail
 
       private
 
+      # @api private
+      def assert_metadatum_key_is_permitted(key)
+        return unless FORBIDDEN_METADATA_KEYS.include?(key.to_sym)
+        raise PaperTrail::InvalidOption,
+          format(E_FORBIDDEN_METADATA_KEY, key, FORBIDDEN_METADATA_KEYS)
+      end
+
       # Rails 5.1 changed the API of `ActiveRecord::Dirty`. See
       # https://github.com/paper-trail-gem/paper_trail/pull/899
       #
@@ -109,19 +129,25 @@ module PaperTrail
         @changed_in_latest_version ||= changes_in_latest_version.keys
       end
 
-      # Rails 5.1 changed the API of `ActiveRecord::Dirty`. See
-      # https://github.com/paper-trail-gem/paper_trail/pull/899
+      # Memoized to reduce memory usage
       #
       # @api private
       def changes_in_latest_version
-        # Memoized to reduce memory usage
-        @changes_in_latest_version ||= begin
-          if @in_after_callback
-            @record.saved_changes
-          else
-            @record.changes
-          end
+        @changes_in_latest_version ||= load_changes_in_latest_version
+      end
+
+      # @api private
+      def evaluate_only
+        only = @record.paper_trail_options[:only].dup
+        # Remove Hash arguments and then evaluate whether the attributes (the
+        # keys of the hash) should also get pushed into the collection.
+        only.delete_if do |obj|
+          obj.is_a?(Hash) &&
+            obj.each { |attr, condition|
+              only << attr if condition.respond_to?(:call) && condition.call(@record)
+            }
         end
+        only
       end
 
       # An attributed is "ignored" if it is listed in the `:ignore` option
@@ -134,6 +160,18 @@ module PaperTrail
         ignored.any? && (changed_in_latest_version & ignored).any?
       end
 
+      # Rails 5.1 changed the API of `ActiveRecord::Dirty`. See
+      # https://github.com/paper-trail-gem/paper_trail/pull/899
+      #
+      # @api private
+      def load_changes_in_latest_version
+        if @in_after_callback
+          @record.saved_changes
+        else
+          @record.changes
+        end
+      end
+
       # PT 10 has a new optional column, `item_subtype`
       #
       # @api private
@@ -157,7 +195,9 @@ module PaperTrail
       #
       # @api private
       def merge_metadata_from_controller_into(data)
-        data.merge(PaperTrail.request.controller_info || {})
+        metadata = PaperTrail.request.controller_info || {}
+        metadata.keys.each { |k| assert_metadatum_key_is_permitted(k) }
+        data.merge(metadata)
       end
 
       # Updates `data` from the model's `meta` option.
@@ -165,6 +205,7 @@ module PaperTrail
       # @api private
       def merge_metadata_from_model_into(data)
         @record.paper_trail_options[:meta].each do |k, v|
+          assert_metadatum_key_is_permitted(k)
           data[k] = model_metadatum(v, data[:event])
         end
       end
@@ -178,24 +219,32 @@ module PaperTrail
         if value.respond_to?(:call)
           value.call(@record)
         elsif value.is_a?(Symbol) && @record.respond_to?(value, true)
-          # If it is an attribute that is changing in an existing object,
-          # be sure to grab the current version.
-          if event != "create" &&
-              @record.has_attribute?(value) &&
-              attribute_changed_in_latest_version?(value)
-            attribute_in_previous_version(value, false)
-          else
-            @record.send(value)
-          end
+          metadatum_from_model_method(event, value)
         else
           value
         end
       end
 
+      # The model method can either be an attribute or a non-attribute method.
+      #
+      # If it is an attribute that is changing in an existing object,
+      # be sure to grab the correct version.
+      #
+      # @api private
+      def metadatum_from_model_method(event, method)
+        if event != "create" &&
+            @record.has_attribute?(method) &&
+            attribute_changed_in_latest_version?(method)
+          attribute_in_previous_version(method, false)
+        else
+          @record.send(method)
+        end
+      end
+
       # @api private
       def notable_changes
         changes_in_latest_version.delete_if { |k, _v|
-          !notably_changed.include?(k)
+          notably_changed.exclude?(k)
         }
       end
 
@@ -203,16 +252,9 @@ module PaperTrail
       def notably_changed
         # Memoized to reduce memory usage
         @notably_changed ||= begin
-          only = @record.paper_trail_options[:only].dup
-          # Remove Hash arguments and then evaluate whether the attributes (the
-          # keys of the hash) should also get pushed into the collection.
-          only.delete_if do |obj|
-            obj.is_a?(Hash) &&
-              obj.each { |attr, condition|
-                only << attr if condition.respond_to?(:call) && condition.call(@record)
-              }
-          end
-          only.empty? ? changed_and_not_ignored : (changed_and_not_ignored & only)
+          only = evaluate_only
+          cani = changed_and_not_ignored
+          only.empty? ? cani : (cani & only)
         end
       end
 
diff --git a/lib/paper_trail/events/update.rb b/lib/paper_trail/events/update.rb
index 8516bc3..3f50c5c 100644
--- a/lib/paper_trail/events/update.rb
+++ b/lib/paper_trail/events/update.rb
@@ -29,22 +29,38 @@ module PaperTrail
           event: @record.paper_trail_event || "update",
           whodunnit: PaperTrail.request.whodunnit
         }
-        if @record.respond_to?(:updated_at)
-          data[:created_at] = @record.updated_at
-        end
         if record_object?
           data[:object] = recordable_object(@is_touch)
         end
-        if record_object_changes?
-          changes = @force_changes.nil? ? notable_changes : @force_changes
-          data[:object_changes] = prepare_object_changes(changes)
-        end
+        merge_object_changes_into(data)
         merge_item_subtype_into(data)
         merge_metadata_into(data)
       end
 
+      # If it is a touch event, and changed are empty, it is assumed to be
+      # implicit `touch` mutation, and will a version is created.
+      #
+      # See https://github.com/rails/rails/commit/dcb825902d79d0f6baba956f7c6ec5767611353e
+      #
+      # @api private
+      def changed_notably?
+        if @is_touch && changes_in_latest_version.empty?
+          true
+        else
+          super
+        end
+      end
+
       private
 
+      # @api private
+      def merge_object_changes_into(data)
+        if record_object_changes?
+          changes = @force_changes.nil? ? notable_changes : @force_changes
+          data[:object_changes] = prepare_object_changes(changes)
+        end
+      end
+
       # `touch` cannot record `object_changes` because rails' `touch` does not
       # perform dirty-tracking. Specifically, methods from `Dirty`, like
       # `saved_changes`, return the same values before and after `touch`.
diff --git a/lib/paper_trail/model_config.rb b/lib/paper_trail/model_config.rb
index 18e6c74..78576a5 100644
--- a/lib/paper_trail/model_config.rb
+++ b/lib/paper_trail/model_config.rb
@@ -18,11 +18,6 @@ module PaperTrail
       `abstract_class`. This is fine, but all application models must be
       configured to use concrete (not abstract) version models.
     STR
-    E_MODEL_LIMIT_REQUIRES_ITEM_SUBTYPE = <<~STR.squish.freeze
-      To use PaperTrail's per-model limit in your %s model, you must have an
-      item_subtype column in your versions table. See documentation sections
-      2.e.1 Per-model limit, and 4.b.1 The optional item_subtype column.
-    STR
     DPR_PASSING_ASSOC_NAME_DIRECTLY_TO_VERSIONS_OPTION = <<~STR.squish
       Passing versions association name as `has_paper_trail versions: %{versions_name}`
       is deprecated. Use `has_paper_trail versions: {name: %{versions_name}}` instead.
@@ -45,22 +40,14 @@ module PaperTrail
       @model_class.after_create { |r|
         r.paper_trail.record_create if r.paper_trail.save_version?
       }
-      return if @model_class.paper_trail_options[:on].include?(:create)
-      @model_class.paper_trail_options[:on] << :create
+      append_option_uniquely(:on, :create)
     end
 
     # Adds a callback that records a version before or after a "destroy" event.
     #
     # @api public
     def on_destroy(recording_order = "before")
-      unless %w[after before].include?(recording_order.to_s)
-        raise ArgumentError, 'recording order can only be "after" or "before"'
-      end
-
-      if recording_order.to_s == "after" && cannot_record_after_destroy?
-        raise E_CANNOT_RECORD_AFTER_DESTROY
-      end
-
+      assert_valid_recording_order_for_on_destroy(recording_order)
       @model_class.send(
         "#{recording_order}_destroy",
         lambda do |r|
@@ -68,9 +55,7 @@ module PaperTrail
           r.paper_trail.record_destroy(recording_order)
         end
       )
-
-      return if @model_class.paper_trail_options[:on].include?(:destroy)
-      @model_class.paper_trail_options[:on] << :destroy
+      append_option_uniquely(:on, :destroy)
     end
 
     # Adds a callback that records a version after an "update" event.
@@ -92,19 +77,27 @@ module PaperTrail
       @model_class.after_update { |r|
         r.paper_trail.clear_version_instance
       }
-      return if @model_class.paper_trail_options[:on].include?(:update)
-      @model_class.paper_trail_options[:on] << :update
+      append_option_uniquely(:on, :update)
     end
 
     # Adds a callback that records a version after a "touch" event.
+    #
+    # Rails < 6.0 has a bug where dirty-tracking does not occur during
+    # a `touch`. (https://github.com/rails/rails/issues/33429) See also:
+    # https://github.com/paper-trail-gem/paper_trail/issues/1121
+    # https://github.com/paper-trail-gem/paper_trail/issues/1161
+    # https://github.com/paper-trail-gem/paper_trail/pull/1285
+    #
     # @api public
     def on_touch
       @model_class.after_touch { |r|
-        r.paper_trail.record_update(
-          force: true,
-          in_after_callback: true,
-          is_touch: true
-        )
+        if r.paper_trail.save_version?
+          r.paper_trail.record_update(
+            force: false,
+            in_after_callback: true,
+            is_touch: true
+          )
+        end
       }
     end
 
@@ -117,7 +110,6 @@ module PaperTrail
       @model_class.send :include, ::PaperTrail::Model::InstanceMethods
       setup_options(options)
       setup_associations(options)
-      check_presence_of_item_subtype_column(options)
       @model_class.after_rollback { paper_trail.clear_rolled_back_versions }
       setup_callbacks_from_options options[:on]
     end
@@ -129,26 +121,34 @@ module PaperTrail
 
     private
 
+    # @api private
+    def append_option_uniquely(option, value)
+      collection = @model_class.paper_trail_options.fetch(option)
+      return if collection.include?(value)
+      collection << value
+    end
+
     # Raises an error if the provided class is an `abstract_class`.
     # @api private
     def assert_concrete_activerecord_class(class_name)
       if class_name.constantize.abstract_class?
-        raise format(E_HPT_ABSTRACT_CLASS, @model_class, class_name)
+        raise Error, format(E_HPT_ABSTRACT_CLASS, @model_class, class_name)
       end
     end
 
-    def cannot_record_after_destroy?
-      ::ActiveRecord::Base.belongs_to_required_by_default
+    # @api private
+    def assert_valid_recording_order_for_on_destroy(recording_order)
+      unless %w[after before].include?(recording_order.to_s)
+        raise ArgumentError, 'recording order can only be "after" or "before"'
+      end
+
+      if recording_order.to_s == "after" && cannot_record_after_destroy?
+        raise Error, E_CANNOT_RECORD_AFTER_DESTROY
+      end
     end
 
-    # Some options require the presence of the `item_subtype` column. Currently
-    # only `limit`, but in the future there may be others.
-    #
-    # @api private
-    def check_presence_of_item_subtype_column(options)
-      return unless options.key?(:limit)
-      return if version_class.item_subtype_column_present?
-      raise format(E_MODEL_LIMIT_REQUIRES_ITEM_SUBTYPE, @model_class.name)
+    def cannot_record_after_destroy?
+      ::ActiveRecord::Base.belongs_to_required_by_default
     end
 
     def check_version_class_name(options)
@@ -206,6 +206,14 @@ module PaperTrail
       options
     end
 
+    # Process an `ignore`, `skip`, or `only` option.
+    def event_attribute_option(option_name)
+      [@model_class.paper_trail_options[option_name]].
+        flatten.
+        compact.
+        map { |attr| attr.is_a?(Hash) ? attr.stringify_keys : attr.to_s }
+    end
+
     def get_versions_scope(options)
       options[:versions][:scope] || -> { order(model.timestamp_sort_order) }
     end
@@ -240,12 +248,8 @@ module PaperTrail
       @model_class.paper_trail_options = options.dup
 
       %i[ignore skip only].each do |k|
-        @model_class.paper_trail_options[k] = [@model_class.paper_trail_options[k]].
-          flatten.
-          compact.
-          map { |attr| attr.is_a?(Hash) ? attr.stringify_keys : attr.to_s }
+        @model_class.paper_trail_options[k] = event_attribute_option(k)
       end
-
       @model_class.paper_trail_options[:meta] ||= {}
     end
   end
diff --git a/lib/paper_trail/queries/versions/where_attribute_changes.rb b/lib/paper_trail/queries/versions/where_attribute_changes.rb
new file mode 100644
index 0000000..6c87590
--- /dev/null
+++ b/lib/paper_trail/queries/versions/where_attribute_changes.rb
@@ -0,0 +1,50 @@
+# frozen_string_literal: true
+
+module PaperTrail
+  module Queries
+    module Versions
+      # For public API documentation, see `where_attribute_changes` in
+      # `paper_trail/version_concern.rb`.
+      # @api private
+      class WhereAttributeChanges
+        # - version_model_class - The class that VersionConcern was mixed into.
+        # - attribute - An attribute that changed. See the public API
+        #   documentation for details.
+        # @api private
+        def initialize(version_model_class, attribute)
+          @version_model_class = version_model_class
+          @attribute = attribute
+        end
+
+        # @api private
+        def execute
+          if PaperTrail.config.object_changes_adapter.respond_to?(:where_attribute_changes)
+            return PaperTrail.config.object_changes_adapter.where_attribute_changes(
+              @version_model_class, @attribute
+            )
+          end
+          column_type = @version_model_class.columns_hash["object_changes"].type
+          case column_type
+          when :jsonb, :json
+            json
+          else
+            raise UnsupportedColumnType.new(
+              method: "where_attribute_changes",
+              expected: "json or jsonb",
+              actual: column_type
+            )
+          end
+        end
+
+        private
+
+        # @api private
+        def json
+          sql = "object_changes -> ? IS NOT NULL"
+
+          @version_model_class.where(sql, @attribute)
+        end
+      end
+    end
+  end
+end
diff --git a/lib/paper_trail/queries/versions/where_object.rb b/lib/paper_trail/queries/versions/where_object.rb
index 7b277da..9fa6319 100644
--- a/lib/paper_trail/queries/versions/where_object.rb
+++ b/lib/paper_trail/queries/versions/where_object.rb
@@ -19,7 +19,7 @@ module PaperTrail
         # @api private
         def execute
           column = @version_model_class.columns_hash["object"]
-          raise "where_object can't be called without an object column" unless column
+          raise Error, "where_object requires an object column" unless column
 
           case column.type
           when :jsonb
diff --git a/lib/paper_trail/queries/versions/where_object_changes.rb b/lib/paper_trail/queries/versions/where_object_changes.rb
index d0de325..5ece2cb 100644
--- a/lib/paper_trail/queries/versions/where_object_changes.rb
+++ b/lib/paper_trail/queries/versions/where_object_changes.rb
@@ -15,7 +15,7 @@ module PaperTrail
           @version_model_class = version_model_class
 
           # Currently, this `deep_dup` is necessary because the `jsonb` branch
-          # modifies `@attributes`, and that would be a nasty suprise for
+          # modifies `@attributes`, and that would be a nasty surprise for
           # consumers of this class.
           # TODO: Stop modifying `@attributes`, then remove `deep_dup`.
           @attributes = attributes.deep_dup
@@ -28,13 +28,18 @@ module PaperTrail
               @version_model_class, @attributes
             )
           end
-          case @version_model_class.columns_hash["object_changes"].type
+          column_type = @version_model_class.columns_hash["object_changes"].type
+          case column_type
           when :jsonb
             jsonb
           when :json
             json
           else
-            text
+            raise UnsupportedColumnType.new(
+              method: "where_object_changes",
+              expected: "json or jsonb",
+              actual: column_type
+            )
           end
         end
 
@@ -59,16 +64,6 @@ module PaperTrail
           @attributes.each { |field, value| @attributes[field] = [value] }
           @version_model_class.where("object_changes @> ?", @attributes.to_json)
         end
-
-        # @api private
-        def text
-          arel_field = @version_model_class.arel_table[:object_changes]
-          where_conditions = @attributes.map { |field, value|
-            ::PaperTrail.serializer.where_object_changes_condition(arel_field, field, value)
-          }
-          where_conditions = where_conditions.reduce { |a, e| a.and(e) }
-          @version_model_class.where(where_conditions)
-        end
       end
     end
   end
diff --git a/lib/paper_trail/queries/versions/where_object_changes_from.rb b/lib/paper_trail/queries/versions/where_object_changes_from.rb
index e3d817b..13aaf09 100644
--- a/lib/paper_trail/queries/versions/where_object_changes_from.rb
+++ b/lib/paper_trail/queries/versions/where_object_changes_from.rb
@@ -23,12 +23,16 @@ module PaperTrail
               @version_model_class, @attributes
             )
           end
-
-          case @version_model_class.columns_hash["object_changes"].type
+          column_type = @version_model_class.columns_hash["object_changes"].type
+          case column_type
           when :jsonb, :json
             json
           else
-            text
+            raise UnsupportedColumnType.new(
+              method: "where_object_changes_from",
+              expected: "json or jsonb",
+              actual: column_type
+            )
           end
         end
 
@@ -47,18 +51,6 @@ module PaperTrail
           sql = predicates.join(" and ")
           @version_model_class.where(sql, *values)
         end
-
-        # @api private
-        def text
-          arel_field = @version_model_class.arel_table[:object_changes]
-
-          where_conditions = @attributes.map do |field, value|
-            ::PaperTrail.serializer.where_object_changes_from_condition(arel_field, field, value)
-          end
-
-          where_conditions = where_conditions.reduce { |a, e| a.and(e) }
-          @version_model_class.where(where_conditions)
-        end
       end
     end
   end
diff --git a/lib/paper_trail/queries/versions/where_object_changes_to.rb b/lib/paper_trail/queries/versions/where_object_changes_to.rb
new file mode 100644
index 0000000..2c9a09b
--- /dev/null
+++ b/lib/paper_trail/queries/versions/where_object_changes_to.rb
@@ -0,0 +1,57 @@
+# frozen_string_literal: true
+
+module PaperTrail
+  module Queries
+    module Versions
+      # For public API documentation, see `where_object_changes_to` in
+      # `paper_trail/version_concern.rb`.
+      # @api private
+      class WhereObjectChangesTo
+        # - version_model_class - The class that VersionConcern was mixed into.
+        # - attributes - A `Hash` of attributes and values. See the public API
+        #   documentation for details.
+        # @api private
+        def initialize(version_model_class, attributes)
+          @version_model_class = version_model_class
+          @attributes = attributes
+        end
+
+        # @api private
+        def execute
+          if PaperTrail.config.object_changes_adapter.respond_to?(:where_object_changes_to)
+            return PaperTrail.config.object_changes_adapter.where_object_changes_to(
+              @version_model_class, @attributes
+            )
+          end
+          column_type = @version_model_class.columns_hash["object_changes"].type
+          case column_type
+          when :jsonb, :json
+            json
+          else
+            raise UnsupportedColumnType.new(
+              method: "where_object_changes_to",
+              expected: "json or jsonb",
+              actual: column_type
+            )
+          end
+        end
+
+        private
+
+        # @api private
+        def json
+          predicates = []
+          values = []
+          @attributes.each do |field, value|
+            predicates.push(
+              "(object_changes->>? ILIKE ?)"
+            )
+            values.concat([field, "[%#{value.to_json}]"])
+          end
+          sql = predicates.join(" and ")
+          @version_model_class.where(sql, *values)
+        end
+      end
+    end
+  end
+end
diff --git a/lib/paper_trail/record_trail.rb b/lib/paper_trail/record_trail.rb
index 26708f2..8063b70 100644
--- a/lib/paper_trail/record_trail.rb
+++ b/lib/paper_trail/record_trail.rb
@@ -25,14 +25,6 @@ module PaperTrail
       @record.send("#{@record.class.version_association_name}=", nil)
     end
 
-    # Is PT enabled for this particular record?
-    # @api private
-    def enabled?
-      PaperTrail.enabled? &&
-        PaperTrail.request.enabled? &&
-        PaperTrail.request.enabled_for_model?(@record.class)
-    end
-
     # Returns true if this instance is the current, live one;
     # returns false if this instance came from a previous version.
     def live?
@@ -75,13 +67,6 @@ module PaperTrail
       end
     end
 
-    # PT-AT extends this method to add its transaction id.
-    #
-    # @api private
-    def data_for_create
-      {}
-    end
-
     # `recording_order` is "after" or "before". See ModelConfig#on_destroy.
     #
     # @api private
@@ -105,14 +90,12 @@ module PaperTrail
       end
     end
 
-    # PT-AT extends this method to add its transaction id.
-    #
-    # @api private
-    def data_for_destroy
-      {}
-    end
-
     # @api private
+    # @param force [boolean] Insert a `Version` even if `@record` has not
+    #   `changed_notably?`.
+    # @param in_after_callback [boolean] True when called from an `after_update`
+    #   or `after_touch` callback.
+    # @param is_touch [boolean] True when called from an `after_touch` callback.
     # @return - The created version object, so that plugins can use it, e.g.
     # paper_trail-association_tracking
     def record_update(force:, in_after_callback:, is_touch:)
@@ -136,40 +119,6 @@ module PaperTrail
       end
     end
 
-    # PT-AT extends this method to add its transaction id.
-    #
-    # @api private
-    def data_for_update
-      {}
-    end
-
-    # @api private
-    # @return - The created version object, so that plugins can use it, e.g.
-    # paper_trail-association_tracking
-    def record_update_columns(changes)
-      return unless enabled?
-      event = Events::Update.new(@record, false, false, changes)
-
-      # Merge data from `Event` with data from PT-AT. We no longer use
-      # `data_for_update_columns` but PT-AT still does.
-      data = event.data.merge(data_for_update_columns)
-
-      versions_assoc = @record.send(@record.class.versions_association_name)
-      version = versions_assoc.create(data)
-      if version.errors.any?
-        log_version_errors(version, :update)
-      else
-        version
-      end
-    end
-
-    # PT-AT extends this method to add its transaction id.
-    #
-    # @api private
-    def data_for_update_columns
-      {}
-    end
-
     # Invoked via callback when a user attempts to persist a reified
     # `Version`.
     def reset_timestamp_attrs_for_update_if_needed
@@ -194,15 +143,17 @@ module PaperTrail
     # Save, and create a version record regardless of options such as `:on`,
     # `:if`, or `:unless`.
     #
-    # Arguments are passed to `save`.
+    # `in_after_callback`: Indicates if this method is being called within an
+    #                      `after` callback. Defaults to `false`.
+    # `options`: Optional arguments passed to `save`.
     #
     # This is an "update" event. That is, we record the same data we would in
     # the case of a normal AR `update`.
-    def save_with_version(**options)
+    def save_with_version(in_after_callback: false, **options)
       ::PaperTrail.request(enabled: false) do
         @record.save(**options)
       end
-      record_update(force: true, in_after_callback: false, is_touch: false)
+      record_update(force: true, in_after_callback: in_after_callback, is_touch: false)
     end
 
     # Like the `update_column` method from `ActiveRecord::Persistence`, but also
@@ -267,11 +218,22 @@ module PaperTrail
     def build_version_on_update(force:, in_after_callback:, is_touch:)
       event = Events::Update.new(@record, in_after_callback, is_touch, nil)
       return unless force || event.changed_notably?
+      data = event.data
+
+      # Copy the (recently set) `updated_at` from the record to the `created_at`
+      # of the `Version`. Without this feature, these two timestamps would
+      # differ by a few milliseconds. To some people, it seems a little
+      # unnatural to tamper with creation timestamps in this way. But, this
+      # feature has existed for a long time, almost a decade now, and some users
+      # may rely on it now.
+      if @record.respond_to?(:updated_at)
+        data[:created_at] = @record.updated_at
+      end
 
       # Merge data from `Event` with data from PT-AT. We no longer use
       # `data_for_update` but PT-AT still does. To save memory, we use `merge!`
       # instead of `merge`.
-      data = event.data.merge!(data_for_update)
+      data.merge!(data_for_update)
 
       # Using `version_class.new` reduces memory usage compared to
       # `versions_assoc.build`. It's a trade-off though. We have to clear
@@ -280,13 +242,69 @@ module PaperTrail
       @record.class.paper_trail.version_class.new(data)
     end
 
+    # PT-AT extends this method to add its transaction id.
+    #
+    # @api public
+    def data_for_create
+      {}
+    end
+
+    # PT-AT extends this method to add its transaction id.
+    #
+    # @api public
+    def data_for_destroy
+      {}
+    end
+
+    # PT-AT extends this method to add its transaction id.
+    #
+    # @api public
+    def data_for_update
+      {}
+    end
+
+    # PT-AT extends this method to add its transaction id.
+    #
+    # @api public
+    def data_for_update_columns
+      {}
+    end
+
+    # Is PT enabled for this particular record?
+    # @api private
+    def enabled?
+      PaperTrail.enabled? &&
+        PaperTrail.request.enabled? &&
+        PaperTrail.request.enabled_for_model?(@record.class)
+    end
+
     def log_version_errors(version, action)
       version.logger&.warn(
         "Unable to create version for #{action} of #{@record.class.name}" \
-          "##{@record.id}: " + version.errors.full_messages.join(", ")
+        "##{@record.id}: " + version.errors.full_messages.join(", ")
       )
     end
 
+    # @api private
+    # @return - The created version object, so that plugins can use it, e.g.
+    # paper_trail-association_tracking
+    def record_update_columns(changes)
+      return unless enabled?
+      data = Events::Update.new(@record, false, false, changes).data
+
+      # Merge data from `Event` with data from PT-AT. We no longer use
+      # `data_for_update_columns` but PT-AT still does.
+      data.merge!(data_for_update_columns)
+
+      versions_assoc = @record.send(@record.class.versions_association_name)
+      version = versions_assoc.create(data)
+      if version.errors.any?
+        log_version_errors(version, :update)
+      else
+        version
+      end
+    end
+
     def version
       @record.public_send(@record.class.version_association_name)
     end
diff --git a/lib/paper_trail/reifier.rb b/lib/paper_trail/reifier.rb
index fbb587f..d78f872 100644
--- a/lib/paper_trail/reifier.rb
+++ b/lib/paper_trail/reifier.rb
@@ -60,9 +60,7 @@ module PaperTrail
         model = if options[:dup] == true || version.event == "destroy"
                   klass.new
                 else
-                  find_cond = { klass.primary_key => version.item_id }
-
-                  version.item || klass.unscoped.where(find_cond).first || klass.new
+                  version.item || init_model_by_finding_item_id(klass, version) || klass.new
                 end
 
         if options[:unversioned_attributes] == :nil && !model.new_record?
@@ -72,6 +70,11 @@ module PaperTrail
         model
       end
 
+      # @api private
+      def init_model_by_finding_item_id(klass, version)
+        klass.unscoped.where(klass.primary_key => version.item_id).first
+      end
+
       # Look for attributes that exist in `model` and not in this version.
       # These attributes should be set to nil. Modifies `attrs`.
       # @api private
@@ -109,21 +112,35 @@ module PaperTrail
       end
 
       # Given a `version`, return the class to reify. This method supports
-      # Single Table Inheritance (STI) with custom inheritance columns.
+      # Single Table Inheritance (STI) with custom inheritance columns and
+      # custom inheritance column values.
       #
       # For example, imagine a `version` whose `item_type` is "Animal". The
       # `animals` table is an STI table (it has cats and dogs) and it has a
       # custom inheritance column, `species`. If `attrs["species"]` is "Dog",
       # this method returns the constant `Dog`. If `attrs["species"]` is blank,
-      # this method returns the constant `Animal`. You can see this particular
-      # example in action in `spec/models/animal_spec.rb`.
+      # this method returns the constant `Animal`.
       #
-      # TODO: Duplication: similar `constantize` in VersionConcern#version_limit
+      # The values contained in the inheritance columns may be non-camelized
+      # strings (e.g. 'dog' instead of 'Dog'). To reify classes in this case
+      # we need to call the parents class `sti_class_for` method to retrieve
+      # the correct record class.
+      #
+      # You can see these particular examples in action in
+      # `spec/models/animal_spec.rb` and `spec/models/plant_spec.rb`
       def version_reification_class(version, attrs)
-        inheritance_column_name = version.item_type.constantize.inheritance_column
+        clazz = version.item_type.constantize
+        inheritance_column_name = clazz.inheritance_column
         inher_col_value = attrs[inheritance_column_name]
-        class_name = inher_col_value.blank? ? version.item_type : inher_col_value
-        class_name.constantize
+        return clazz if inher_col_value.blank?
+
+        # Rails 6.1 adds a public method for clients to use to customize STI classes. If that
+        # method is not available, fall back to using the private one
+        if clazz.public_methods.include?(:sti_class_for)
+          return clazz.sti_class_for(inher_col_value)
+        end
+
+        clazz.send(:find_sti_class, inher_col_value)
       end
     end
   end
diff --git a/lib/paper_trail/request.rb b/lib/paper_trail/request.rb
index eb713c4..dbc4e87 100644
--- a/lib/paper_trail/request.rb
+++ b/lib/paper_trail/request.rb
@@ -12,9 +12,6 @@ module PaperTrail
   #
   # @api private
   module Request
-    class InvalidOption < RuntimeError
-    end
-
     class << self
       # Sets any data from the controller that you want PaperTrail to store.
       # See also `PaperTrail::Rails::Controller#info_for_paper_trail`.
@@ -78,28 +75,6 @@ module PaperTrail
           !!store.fetch(:"enabled_for_#{model}", true)
       end
 
-      # @api private
-      def merge(options)
-        options.to_h.each do |k, v|
-          store[k] = v
-        end
-      end
-
-      # @api private
-      def set(options)
-        store.clear
-        merge(options)
-      end
-
-      # Returns a deep copy of the internal hash from our RequestStore. Keys are
-      # all symbols. Values are mostly primitives, but whodunnit can be a Proc.
-      # We cannot use Marshal.dump here because it doesn't support Proc. It is
-      # unclear exactly how `deep_dup` handles a Proc, but it doesn't complain.
-      # @api private
-      def to_h
-        store.deep_dup
-      end
-
       # Temporarily set `options` and execute a block.
       # @api private
       def with(options)
@@ -136,6 +111,19 @@ module PaperTrail
 
       private
 
+      # @api private
+      def merge(options)
+        options.to_h.each do |k, v|
+          store[k] = v
+        end
+      end
+
+      # @api private
+      def set(options)
+        store.clear
+        merge(options)
+      end
+
       # Returns a Hash, initializing with default values if necessary.
       # @api private
       def store
@@ -144,6 +132,15 @@ module PaperTrail
         }
       end
 
+      # Returns a deep copy of the internal hash from our RequestStore. Keys are
+      # all symbols. Values are mostly primitives, but whodunnit can be a Proc.
+      # We cannot use Marshal.dump here because it doesn't support Proc. It is
+      # unclear exactly how `deep_dup` handles a Proc, but it doesn't complain.
+      # @api private
+      def to_h
+        store.deep_dup
+      end
+
       # Provide a helpful error message if someone has a typo in one of their
       # option keys. We don't validate option values here. That's traditionally
       # been handled with casting (`to_s`, `!!`) in the accessor method.
diff --git a/lib/paper_trail/serializers/json.rb b/lib/paper_trail/serializers/json.rb
index 0a58f3e..f0cbf89 100644
--- a/lib/paper_trail/serializers/json.rb
+++ b/lib/paper_trail/serializers/json.rb
@@ -31,24 +31,6 @@ module PaperTrail
           arel_field.matches("%\"#{field}\":#{json_value}%")
         end
       end
-
-      def where_object_changes_condition(*)
-        raise <<-STR.squish.freeze
-          where_object_changes no longer supports reading JSON from a text
-          column. The old implementation was inaccurate, returning more records
-          than you wanted. This feature was deprecated in 7.1.0 and removed in
-          8.0.0. The json and jsonb datatypes are still supported. See the
-          discussion at https://github.com/paper-trail-gem/paper_trail/issues/803
-        STR
-      end
-
-      # Raises an exception as this operation is not allowed from text columns.
-      def where_object_changes_from_condition(*)
-        raise <<-STR.squish.freeze
-          where_object_changes_from does not support reading JSON from a text
-          column. The json and jsonb datatypes are supported.
-        STR
-      end
     end
   end
 end
diff --git a/lib/paper_trail/serializers/yaml.rb b/lib/paper_trail/serializers/yaml.rb
index 666b4dd..9e6bc03 100644
--- a/lib/paper_trail/serializers/yaml.rb
+++ b/lib/paper_trail/serializers/yaml.rb
@@ -9,13 +9,23 @@ module PaperTrail
       extend self # makes all instance methods become module methods as well
 
       def load(string)
-        ::YAML.load string
+        if use_safe_load?
+          ::YAML.safe_load(
+            string,
+            permitted_classes: yaml_column_permitted_classes,
+            aliases: true
+          )
+        elsif ::YAML.respond_to?(:unsafe_load)
+          ::YAML.unsafe_load(string)
+        else
+          ::YAML.load(string)
+        end
       end
 
       # @param object (Hash | HashWithIndifferentAccess) - Coming from
       # `recordable_object` `object` will be a plain `Hash`. However, due to
-      # recent [memory optimizations](https://git.io/fjeYv), when coming from
-      # `recordable_object_changes`, it will be a `HashWithIndifferentAccess`.
+      # recent [memory optimizations](https://github.com/paper-trail-gem/paper_trail/pull/1189),
+      # when coming from `recordable_object_changes`, it will be a `HashWithIndifferentAccess`.
       def dump(object)
         object = object.to_hash if object.is_a?(HashWithIndifferentAccess)
         ::YAML.dump object
@@ -27,24 +37,31 @@ module PaperTrail
         arel_field.matches("%\n#{field}: #{value}\n%")
       end
 
-      # Returns a SQL LIKE condition to be used to match the given field and
-      # value in the serialized `object_changes`.
-      def where_object_changes_condition(*)
-        raise <<-STR.squish.freeze
-          where_object_changes no longer supports reading YAML from a text
-          column. The old implementation was inaccurate, returning more records
-          than you wanted. This feature was deprecated in 8.1.0 and removed in
-          9.0.0. The json and jsonb datatypes are still supported. See
-          discussion at https://github.com/paper-trail-gem/paper_trail/pull/997
-        STR
+      private
+
+      def use_safe_load?
+        if ::ActiveRecord.gem_version >= Gem::Version.new("7.0.3.1")
+          # `use_yaml_unsafe_load` may be removed in the future, at which point
+          # safe loading will be the default.
+          !defined?(ActiveRecord.use_yaml_unsafe_load) || !ActiveRecord.use_yaml_unsafe_load
+        elsif defined?(ActiveRecord::Base.use_yaml_unsafe_load)
+          # Rails 5.2.8.1, 6.0.5.1, 6.1.6.1
+          !ActiveRecord::Base.use_yaml_unsafe_load
+        else
+          false
+        end
       end
 
-      # Raises an exception as this operation is not allowed with YAML.
-      def where_object_changes_from_condition(*)
-        raise <<-STR.squish.freeze
-          where_object_changes_from does not support reading YAML from a text
-          column. The json and jsonb datatypes are supported.
-        STR
+      def yaml_column_permitted_classes
+        if defined?(ActiveRecord.yaml_column_permitted_classes)
+          # Rails >= 7.0.3.1
+          ActiveRecord.yaml_column_permitted_classes
+        elsif defined?(ActiveRecord::Base.yaml_column_permitted_classes)
+          # Rails 5.2.8.1, 6.0.5.1, 6.1.6.1
+          ActiveRecord::Base.yaml_column_permitted_classes
+        else
+          []
+        end
       end
     end
   end
diff --git a/lib/paper_trail/version_concern.rb b/lib/paper_trail/version_concern.rb
index 429ac60..c49bfe0 100644
--- a/lib/paper_trail/version_concern.rb
+++ b/lib/paper_trail/version_concern.rb
@@ -1,9 +1,11 @@
 # frozen_string_literal: true
 
 require "paper_trail/attribute_serializers/object_changes_attribute"
+require "paper_trail/queries/versions/where_attribute_changes"
 require "paper_trail/queries/versions/where_object"
 require "paper_trail/queries/versions/where_object_changes"
 require "paper_trail/queries/versions/where_object_changes_from"
+require "paper_trail/queries/versions/where_object_changes_to"
 
 module PaperTrail
   # Originally, PaperTrail did not provide this module, and all of this
@@ -13,18 +15,20 @@ module PaperTrail
   module VersionConcern
     extend ::ActiveSupport::Concern
 
+    E_YAML_PERMITTED_CLASSES = <<-EOS.squish.freeze
+      PaperTrail encountered a Psych::DisallowedClass error during
+      deserialization of YAML column, indicating that
+      yaml_column_permitted_classes has not been configured correctly. %s
+    EOS
+
     included do
-      belongs_to :item, polymorphic: true, optional: true
+      belongs_to :item, polymorphic: true, optional: true, inverse_of: false
       validates_presence_of :event
       after_create :enforce_version_limit!
     end
 
     # :nodoc:
     module ClassMethods
-      def item_subtype_column_present?
-        column_names.include?("item_subtype")
-      end
-
       def with_item_keys(item_type, item_id)
         where item_type: item_type, item_id: item_id
       end
@@ -42,7 +46,7 @@ module PaperTrail
       end
 
       def not_creates
-        where "event <> ?", "create"
+        where.not(event: "create")
       end
 
       def between(start_time, end_time)
@@ -60,6 +64,18 @@ module PaperTrail
         end
       end
 
+      # Given an attribute like `"name"`, query the `versions.object_changes`
+      # column for any changes that modified the provided attribute.
+      #
+      # @api public
+      def where_attribute_changes(attribute)
+        unless attribute.is_a?(String) || attribute.is_a?(Symbol)
+          raise ArgumentError, "expected to receive a String or Symbol"
+        end
+
+        Queries::Versions::WhereAttributeChanges.new(self, attribute).execute
+      end
+
       # Given a hash of attributes like `name: 'Joan'`, query the
       # `versions.objects` column.
       #
@@ -131,6 +147,21 @@ module PaperTrail
         Queries::Versions::WhereObjectChangesFrom.new(self, args).execute
       end
 
+      # Given a hash of attributes like `name: 'Joan'`, query the
+      # `versions.objects_changes` column for changes where the version changed
+      # to the hash of attributes from other values.
+      #
+      # This is useful for finding versions where the attribute started with an
+      # unknown value and changed to a known value. This is in comparison to
+      # `where_object_changes` which will find both the changes before and
+      # after.
+      #
+      # @api public
+      def where_object_changes_to(args = {})
+        raise ArgumentError, "expected to receive a Hash" unless args.is_a?(Hash)
+        Queries::Versions::WhereObjectChangesTo.new(self, args).execute
+      end
+
       def primary_key_is_int?
         @primary_key_is_int ||= columns_hash[primary_key].type == :integer
       rescue StandardError # TODO: Rescue something more specific
@@ -237,7 +268,7 @@ module PaperTrail
     #
     def reify(options = {})
       unless self.class.column_names.include? "object"
-        raise "reify can't be called without an object column"
+        raise Error, "reify requires an object column"
       end
       return nil if object.nil?
       ::PaperTrail::Reifier.reify(self, options)
@@ -323,7 +354,10 @@ module PaperTrail
       else
         begin
           PaperTrail.serializer.load(object_changes)
-        rescue StandardError # TODO: Rescue something more specific
+        rescue StandardError => e
+          if defined?(::Psych::Exception) && e.instance_of?(::Psych::Exception)
+            ::Kernel.warn format(E_YAML_PERMITTED_CLASSES, e)
+          end
           {}
         end
       end
@@ -350,16 +384,23 @@ module PaperTrail
     # The version limit can be global or per-model.
     #
     # @api private
-    #
-    # TODO: Duplication: similar `constantize` in Reifier#version_reification_class
     def version_limit
-      if self.class.item_subtype_column_present?
-        klass = (item_subtype || item_type).constantize
-        if klass&.paper_trail_options&.key?(:limit)
-          return klass.paper_trail_options[:limit]
-        end
+      klass = item.class
+      if limit_option?(klass)
+        klass.paper_trail_options[:limit]
+      elsif base_class_limit_option?(klass)
+        klass.base_class.paper_trail_options[:limit]
+      else
+        PaperTrail.config.version_limit
       end
-      PaperTrail.config.version_limit
+    end
+
+    def limit_option?(klass)
+      klass.respond_to?(:paper_trail_options) && klass.paper_trail_options.key?(:limit)
+    end
+
+    def base_class_limit_option?(klass)
+      klass.respond_to?(:base_class) && limit_option?(klass.base_class)
     end
   end
 end
diff --git a/lib/paper_trail/version_number.rb b/lib/paper_trail/version_number.rb
index cf2cfae..1f8f3ea 100644
--- a/lib/paper_trail/version_number.rb
+++ b/lib/paper_trail/version_number.rb
@@ -7,7 +7,7 @@ module PaperTrail
   # because of this confusion, but it's not worth the breaking change.
   # People are encouraged to use `PaperTrail.gem_version` instead.
   module VERSION
-    MAJOR = 12
+    MAJOR = 14
     MINOR = 0
     TINY = 0
 
diff --git a/paper_trail.gemspec b/paper_trail.gemspec
index c67c5aa..0ea8b65 100644
--- a/paper_trail.gemspec
+++ b/paper_trail.gemspec
@@ -43,20 +43,20 @@ has been destroyed.
   # about 3 years, per https://www.ruby-lang.org/en/downloads/branches/
   #
   # See "Lowest supported ruby version" in CONTRIBUTING.md
-  #
-  # Ruby 2.5 reaches EoL on 2021-03-31.
-  s.required_ruby_version = ">= 2.5.0"
+  s.required_ruby_version = ">= 2.7.0"
 
   # We no longer specify a maximum activerecord version.
   # See discussion in paper_trail/compatibility.rb
   s.add_dependency "activerecord", ::PaperTrail::Compatibility::ACTIVERECORD_GTE
-  s.add_dependency "request_store", "~> 1.1"
 
-  s.add_development_dependency "appraisal", "~> 2.2"
-  s.add_development_dependency "byebug", "~> 11.0"
-  s.add_development_dependency "ffaker", "~> 2.11"
+  # PT supports request_store versions for 3 years.
+  s.add_dependency "request_store", "~> 1.4"
+
+  s.add_development_dependency "appraisal", "~> 2.4.1"
+  s.add_development_dependency "byebug", "~> 11.1"
+  s.add_development_dependency "ffaker", "~> 2.20"
   s.add_development_dependency "generator_spec", "~> 0.9.4"
-  s.add_development_dependency "memory_profiler", "~> 0.9.14"
+  s.add_development_dependency "memory_profiler", "~> 1.0.0"
 
   # For `spec/dummy_app`. Technically, we only need `actionpack` (as of 2020).
   # However, that might change in the future, and the advantages of specifying a
@@ -64,12 +64,14 @@ has been destroyed.
   s.add_development_dependency "rails", ::PaperTrail::Compatibility::ACTIVERECORD_GTE
 
   s.add_development_dependency "rake", "~> 13.0"
-  s.add_development_dependency "rspec-rails", "~> 4.0"
-  s.add_development_dependency "rubocop", "~> 1.11.0"
-  s.add_development_dependency "rubocop-rails", "~> 2.9.1"
+  s.add_development_dependency "rspec-rails", "~> 5.0.2"
+  s.add_development_dependency "rubocop", "~> 1.22.2"
   s.add_development_dependency "rubocop-packaging", "~> 0.5.1"
-  s.add_development_dependency "rubocop-performance", "~> 1.10.1"
-  s.add_development_dependency "rubocop-rspec", "~> 2.2.0"
+  s.add_development_dependency "rubocop-performance", "~> 1.11.5"
+  s.add_development_dependency "rubocop-rails", "~> 2.12.4"
+  s.add_development_dependency "rubocop-rake", "~> 0.6.0"
+  s.add_development_dependency "rubocop-rspec", "~> 2.5.0"
+  s.add_development_dependency "simplecov", "~> 0.21.2"
 
   # ## Database Adapters
   #
@@ -81,7 +83,7 @@ has been destroyed.
   # Currently, all versions of rails we test against are consistent. In the past,
   # when we tested against rails 4.2, we had to specify database adapters in
   # `Appraisals`.
-  s.add_development_dependency "mysql2", "~> 0.5"
-  s.add_development_dependency "pg", ">= 0.18", "< 2.0"
+  s.add_development_dependency "mysql2", "~> 0.5.3"
+  s.add_development_dependency "pg", "~> 1.2"
   s.add_development_dependency "sqlite3", "~> 1.4"
 end
diff --git a/spec/controllers/articles_controller_spec.rb b/spec/controllers/articles_controller_spec.rb
index dee8c0d..15d11c3 100644
--- a/spec/controllers/articles_controller_spec.rb
+++ b/spec/controllers/articles_controller_spec.rb
@@ -4,7 +4,7 @@ require "spec_helper"
 
 RSpec.describe ArticlesController, type: :controller do
   describe "PaperTrail.request.enabled?" do
-    context "PaperTrail.enabled? == true" do
+    context "when PaperTrail.enabled? == true" do
       before { PaperTrail.enabled = true }
 
       after { PaperTrail.enabled = false }
@@ -18,7 +18,7 @@ RSpec.describe ArticlesController, type: :controller do
       end
     end
 
-    context "PaperTrail.enabled? == false" do
+    context "when PaperTrail.enabled? == false" do
       it "returns false" do
         expect(PaperTrail.enabled?).to eq(false)
         post :create, params: { article: { title: "Doh", content: FFaker::Lorem.sentence } }
diff --git a/spec/controllers/widgets_controller_spec.rb b/spec/controllers/widgets_controller_spec.rb
index 598dca8..ae24ff4 100644
--- a/spec/controllers/widgets_controller_spec.rb
+++ b/spec/controllers/widgets_controller_spec.rb
@@ -8,7 +8,7 @@ RSpec.describe WidgetsController, type: :controller, versioning: true do
   after { RequestStore.store[:paper_trail] = nil }
 
   describe "#create" do
-    context "PT enabled" do
+    context "with PT enabled" do
       it "stores information like IP address in version" do
         post(:create, params: { widget: { name: "Flugel" } })
         widget = assigns(:widget)
@@ -29,7 +29,7 @@ RSpec.describe WidgetsController, type: :controller, versioning: true do
       end
     end
 
-    context "PT disabled" do
+    context "with PT disabled" do
       it "does not save a version, and metadata is not set" do
         request.env["HTTP_USER_AGENT"] = "Disable User-Agent"
         post :create, params: { widget: { name: "Flugel" } }
diff --git a/spec/dummy_app/app/controllers/application_controller.rb b/spec/dummy_app/app/controllers/application_controller.rb
index 9283887..bb999f0 100644
--- a/spec/dummy_app/app/controllers/application_controller.rb
+++ b/spec/dummy_app/app/controllers/application_controller.rb
@@ -9,7 +9,7 @@ class ApplicationController < ActionController::Base
   before_action :modify_current_user
 
   # PT used to add this callback automatically. Now people are required to add
-  # it themsevles, like this, allowing them to control the order of callbacks.
+  # it themselves, like this, allowing them to control the order of callbacks.
   # The `modify_current_user` callback above shows why this control is useful.
   before_action :set_paper_trail_whodunnit
 
diff --git a/spec/dummy_app/app/controllers/test_controller.rb b/spec/dummy_app/app/controllers/test_controller.rb
index da41c69..04a6681 100644
--- a/spec/dummy_app/app/controllers/test_controller.rb
+++ b/spec/dummy_app/app/controllers/test_controller.rb
@@ -1,6 +1,6 @@
 # frozen_string_literal: true
 
-class TestController < ActionController::Base
+class TestController < ApplicationController
   def user_for_paper_trail
     Thread.current.object_id
   end
diff --git a/spec/dummy_app/app/models/animal.rb b/spec/dummy_app/app/models/animal.rb
index 748c68f..4b262f9 100644
--- a/spec/dummy_app/app/models/animal.rb
+++ b/spec/dummy_app/app/models/animal.rb
@@ -1,6 +1,6 @@
 # frozen_string_literal: true
 
-class Animal < ActiveRecord::Base
+class Animal < ApplicationRecord
   has_paper_trail
   self.inheritance_column = "species"
 end
diff --git a/spec/dummy_app/app/models/application_record.rb b/spec/dummy_app/app/models/application_record.rb
new file mode 100644
index 0000000..71fbba5
--- /dev/null
+++ b/spec/dummy_app/app/models/application_record.rb
@@ -0,0 +1,5 @@
+# frozen_string_literal: true
+
+class ApplicationRecord < ActiveRecord::Base
+  self.abstract_class = true
+end
diff --git a/spec/dummy_app/app/models/article.rb b/spec/dummy_app/app/models/article.rb
index 8096c89..a809bdf 100644
--- a/spec/dummy_app/app/models/article.rb
+++ b/spec/dummy_app/app/models/article.rb
@@ -1,7 +1,7 @@
 # frozen_string_literal: true
 
 # Demonstrates the `only` and `ignore` attributes, among other things.
-class Article < ActiveRecord::Base
+class Article < ApplicationRecord
   has_paper_trail(
     ignore: [
       :title, {
diff --git a/spec/dummy_app/app/models/authorship.rb b/spec/dummy_app/app/models/authorship.rb
index d56dd15..33fc1ed 100644
--- a/spec/dummy_app/app/models/authorship.rb
+++ b/spec/dummy_app/app/models/authorship.rb
@@ -1,6 +1,6 @@
 # frozen_string_literal: true
 
-class Authorship < ActiveRecord::Base
+class Authorship < ApplicationRecord
   belongs_to :book
   belongs_to :author, class_name: "Person"
   has_paper_trail
diff --git a/spec/dummy_app/app/models/bar_habtm.rb b/spec/dummy_app/app/models/bar_habtm.rb
index 5cd81fa..4083fac 100644
--- a/spec/dummy_app/app/models/bar_habtm.rb
+++ b/spec/dummy_app/app/models/bar_habtm.rb
@@ -1,6 +1,6 @@
 # frozen_string_literal: true
 
-class BarHabtm < ActiveRecord::Base
+class BarHabtm < ApplicationRecord
   has_and_belongs_to_many :foo_habtms
   has_paper_trail
 end
diff --git a/spec/dummy_app/app/models/book.rb b/spec/dummy_app/app/models/book.rb
index f697a83..196147f 100644
--- a/spec/dummy_app/app/models/book.rb
+++ b/spec/dummy_app/app/models/book.rb
@@ -1,6 +1,6 @@
 # frozen_string_literal: true
 
-class Book < ActiveRecord::Base
+class Book < ApplicationRecord
   has_many :authorships, dependent: :destroy
   has_many :authors, through: :authorships
 
diff --git a/spec/dummy_app/app/models/boolit.rb b/spec/dummy_app/app/models/boolit.rb
index 28bed19..ec5cb55 100644
--- a/spec/dummy_app/app/models/boolit.rb
+++ b/spec/dummy_app/app/models/boolit.rb
@@ -1,6 +1,6 @@
 # frozen_string_literal: true
 
-class Boolit < ActiveRecord::Base
+class Boolit < ApplicationRecord
   default_scope { where(scoped: true) }
   has_paper_trail
 end
diff --git a/spec/dummy_app/app/models/callback_modifier.rb b/spec/dummy_app/app/models/callback_modifier.rb
index dc33217..a287f53 100644
--- a/spec/dummy_app/app/models/callback_modifier.rb
+++ b/spec/dummy_app/app/models/callback_modifier.rb
@@ -1,6 +1,6 @@
 # frozen_string_literal: true
 
-class CallbackModifier < ActiveRecord::Base
+class CallbackModifier < ApplicationRecord
   has_paper_trail on: []
 
   def test_destroy
diff --git a/spec/dummy_app/app/models/car.rb b/spec/dummy_app/app/models/car.rb
index ea3faa5..a6064a8 100644
--- a/spec/dummy_app/app/models/car.rb
+++ b/spec/dummy_app/app/models/car.rb
@@ -2,4 +2,6 @@
 
 class Car < Vehicle
   has_paper_trail
+  attribute :color, type: ActiveModel::Type::String
+  attr_accessor :top_speed
 end
diff --git a/spec/dummy_app/app/models/chapter.rb b/spec/dummy_app/app/models/chapter.rb
index 6a0c92f..b0c7ca0 100644
--- a/spec/dummy_app/app/models/chapter.rb
+++ b/spec/dummy_app/app/models/chapter.rb
@@ -1,6 +1,6 @@
 # frozen_string_literal: true
 
-class Chapter < ActiveRecord::Base
+class Chapter < ApplicationRecord
   has_many :sections, dependent: :destroy
   has_many :paragraphs, through: :sections
 
diff --git a/spec/dummy_app/app/models/citation.rb b/spec/dummy_app/app/models/citation.rb
index bf372de..134d9b7 100644
--- a/spec/dummy_app/app/models/citation.rb
+++ b/spec/dummy_app/app/models/citation.rb
@@ -1,6 +1,6 @@
 # frozen_string_literal: true
 
-class Citation < ActiveRecord::Base
+class Citation < ApplicationRecord
   belongs_to :quotation
 
   has_paper_trail
diff --git a/spec/dummy_app/app/models/custom_primary_key_record.rb b/spec/dummy_app/app/models/custom_primary_key_record.rb
index f72f5f3..329ae0b 100644
--- a/spec/dummy_app/app/models/custom_primary_key_record.rb
+++ b/spec/dummy_app/app/models/custom_primary_key_record.rb
@@ -2,7 +2,7 @@
 
 require "securerandom"
 
-class CustomPrimaryKeyRecord < ActiveRecord::Base
+class CustomPrimaryKeyRecord < ApplicationRecord
   self.primary_key = :uuid
 
   has_paper_trail versions: { class_name: "CustomPrimaryKeyRecordVersion" }
diff --git a/spec/dummy_app/app/models/customer.rb b/spec/dummy_app/app/models/customer.rb
index c487104..f73de64 100644
--- a/spec/dummy_app/app/models/customer.rb
+++ b/spec/dummy_app/app/models/customer.rb
@@ -1,6 +1,6 @@
 # frozen_string_literal: true
 
-class Customer < ActiveRecord::Base
+class Customer < ApplicationRecord
   has_many :orders, dependent: :destroy
   has_paper_trail
 end
diff --git a/spec/dummy_app/app/models/document.rb b/spec/dummy_app/app/models/document.rb
index 2197029..a017dfc 100644
--- a/spec/dummy_app/app/models/document.rb
+++ b/spec/dummy_app/app/models/document.rb
@@ -2,7 +2,7 @@
 
 # Demonstrates a "custom versions association name". Instead of the association
 # being named `versions`, it will be named `paper_trail_versions`.
-class Document < ActiveRecord::Base
+class Document < ApplicationRecord
   has_paper_trail(
     versions: { name: :paper_trail_versions },
     on: %i[create update]
diff --git a/spec/dummy_app/app/models/editor.rb b/spec/dummy_app/app/models/editor.rb
index 070f88e..3c810e7 100644
--- a/spec/dummy_app/app/models/editor.rb
+++ b/spec/dummy_app/app/models/editor.rb
@@ -1,6 +1,6 @@
 # frozen_string_literal: true
 
 # to demonstrate a has_through association that does not have paper_trail enabled
-class Editor < ActiveRecord::Base
+class Editor < ApplicationRecord
   has_many :editorships, dependent: :destroy
 end
diff --git a/spec/dummy_app/app/models/editorship.rb b/spec/dummy_app/app/models/editorship.rb
index aa37fec..588129d 100644
--- a/spec/dummy_app/app/models/editorship.rb
+++ b/spec/dummy_app/app/models/editorship.rb
@@ -1,6 +1,6 @@
 # frozen_string_literal: true
 
-class Editorship < ActiveRecord::Base
+class Editorship < ApplicationRecord
   belongs_to :book
   belongs_to :editor
   has_paper_trail
diff --git a/spec/dummy_app/app/models/family/family.rb b/spec/dummy_app/app/models/family/family.rb
index f13bbc2..7a1e3ce 100644
--- a/spec/dummy_app/app/models/family/family.rb
+++ b/spec/dummy_app/app/models/family/family.rb
@@ -1,14 +1,14 @@
 # frozen_string_literal: true
 
 module Family
-  class Family < ActiveRecord::Base
+  class Family < ApplicationRecord
     has_paper_trail
 
     has_many :familie_lines, class_name: "::Family::FamilyLine", foreign_key: :parent_id
     has_many :children, class_name: "::Family::Family", foreign_key: :parent_id
     has_many :grandsons, through: :familie_lines
     has_one :mentee, class_name: "::Family::Family", foreign_key: :partner_id
-    belongs_to :parent, class_name: "::Family::Family", foreign_key: :parent_id, optional: true
+    belongs_to :parent, class_name: "::Family::Family", optional: true
     belongs_to :mentor, class_name: "::Family::Family", foreign_key: :partner_id, optional: true
 
     accepts_nested_attributes_for :mentee
diff --git a/spec/dummy_app/app/models/family/family_line.rb b/spec/dummy_app/app/models/family/family_line.rb
index c0679b0..93bd4d9 100644
--- a/spec/dummy_app/app/models/family/family_line.rb
+++ b/spec/dummy_app/app/models/family/family_line.rb
@@ -1,15 +1,13 @@
 # frozen_string_literal: true
 
 module Family
-  class FamilyLine < ActiveRecord::Base
+  class FamilyLine < ApplicationRecord
     has_paper_trail
     belongs_to :parent,
       class_name: "::Family::Family",
-      foreign_key: :parent_id,
       optional: true
     belongs_to :grandson,
       class_name: "::Family::Family",
-      foreign_key: :grandson_id,
       optional: true
   end
 end
diff --git a/spec/dummy_app/app/models/fluxor.rb b/spec/dummy_app/app/models/fluxor.rb
index f596a61..fe9fe1c 100644
--- a/spec/dummy_app/app/models/fluxor.rb
+++ b/spec/dummy_app/app/models/fluxor.rb
@@ -1,5 +1,5 @@
 # frozen_string_literal: true
 
-class Fluxor < ActiveRecord::Base
+class Fluxor < ApplicationRecord
   belongs_to :widget, optional: true
 end
diff --git a/spec/dummy_app/app/models/foo_habtm.rb b/spec/dummy_app/app/models/foo_habtm.rb
index d101418..7df26fa 100644
--- a/spec/dummy_app/app/models/foo_habtm.rb
+++ b/spec/dummy_app/app/models/foo_habtm.rb
@@ -1,6 +1,6 @@
 # frozen_string_literal: true
 
-class FooHabtm < ActiveRecord::Base
+class FooHabtm < ApplicationRecord
   has_and_belongs_to_many :bar_habtms
   accepts_nested_attributes_for :bar_habtms
   has_paper_trail
diff --git a/spec/dummy_app/app/models/fruit.rb b/spec/dummy_app/app/models/fruit.rb
index 1237612..958f1ee 100644
--- a/spec/dummy_app/app/models/fruit.rb
+++ b/spec/dummy_app/app/models/fruit.rb
@@ -1,7 +1,8 @@
 # frozen_string_literal: true
 
-class Fruit < ActiveRecord::Base
-  if ENV["DB"] == "postgres" || JsonVersion.table_exists?
+# See also `Vegetable` which uses `JsonbVersion`.
+class Fruit < ApplicationRecord
+  if ENV["DB"] == "postgres"
     has_paper_trail versions: { class_name: "JsonVersion" }
   end
 end
diff --git a/spec/dummy_app/app/models/gadget.rb b/spec/dummy_app/app/models/gadget.rb
index 9a2f663..a845300 100644
--- a/spec/dummy_app/app/models/gadget.rb
+++ b/spec/dummy_app/app/models/gadget.rb
@@ -1,5 +1,5 @@
 # frozen_string_literal: true
 
-class Gadget < ActiveRecord::Base
+class Gadget < ApplicationRecord
   has_paper_trail ignore: [:brand, { color: proc { |obj| obj.color == "Yellow" } }]
 end
diff --git a/spec/dummy_app/app/models/kitchen/banana.rb b/spec/dummy_app/app/models/kitchen/banana.rb
index ed2a4f1..024fbfe 100644
--- a/spec/dummy_app/app/models/kitchen/banana.rb
+++ b/spec/dummy_app/app/models/kitchen/banana.rb
@@ -1,7 +1,7 @@
 # frozen_string_literal: true
 
 module Kitchen
-  class Banana < ActiveRecord::Base
+  class Banana < ApplicationRecord
     has_paper_trail versions: { class_name: "Kitchen::BananaVersion" }
   end
 end
diff --git a/spec/dummy_app/app/models/legacy_widget.rb b/spec/dummy_app/app/models/legacy_widget.rb
index ff2e526..31e24f3 100644
--- a/spec/dummy_app/app/models/legacy_widget.rb
+++ b/spec/dummy_app/app/models/legacy_widget.rb
@@ -3,6 +3,6 @@
 # The `legacy_widgets` table has a `version` column that would conflict with our
 # `version` method. It is configured to define a method named `custom_version`
 # instead.
-class LegacyWidget < ActiveRecord::Base
+class LegacyWidget < ApplicationRecord
   has_paper_trail ignore: :version, version: "custom_version"
 end
diff --git a/spec/dummy_app/app/models/line_item.rb b/spec/dummy_app/app/models/line_item.rb
index 08b720e..639aa57 100644
--- a/spec/dummy_app/app/models/line_item.rb
+++ b/spec/dummy_app/app/models/line_item.rb
@@ -1,6 +1,6 @@
 # frozen_string_literal: true
 
-class LineItem < ActiveRecord::Base
+class LineItem < ApplicationRecord
   belongs_to :order, dependent: :destroy
   has_paper_trail
 end
diff --git a/spec/dummy_app/app/models/no_object.rb b/spec/dummy_app/app/models/no_object.rb
index cf75832..7eb0b87 100644
--- a/spec/dummy_app/app/models/no_object.rb
+++ b/spec/dummy_app/app/models/no_object.rb
@@ -1,7 +1,7 @@
 # frozen_string_literal: true
 
 # Demonstrates a table that omits the `object` column.
-class NoObject < ActiveRecord::Base
+class NoObject < ApplicationRecord
   has_paper_trail(
     versions: { class_name: "NoObjectVersion" },
     meta: { metadatum: 42 }
diff --git a/spec/dummy_app/app/models/not_on_update.rb b/spec/dummy_app/app/models/not_on_update.rb
index fc95bd2..f511908 100644
--- a/spec/dummy_app/app/models/not_on_update.rb
+++ b/spec/dummy_app/app/models/not_on_update.rb
@@ -1,6 +1,6 @@
 # frozen_string_literal: true
 
 # This model does not record versions when updated.
-class NotOnUpdate < ActiveRecord::Base
+class NotOnUpdate < ApplicationRecord
   has_paper_trail on: %i[create destroy]
 end
diff --git a/spec/dummy_app/app/models/on/create.rb b/spec/dummy_app/app/models/on/create.rb
index 626e344..e47c257 100644
--- a/spec/dummy_app/app/models/on/create.rb
+++ b/spec/dummy_app/app/models/on/create.rb
@@ -1,7 +1,7 @@
 # frozen_string_literal: true
 
 module On
-  class Create < ActiveRecord::Base
+  class Create < ApplicationRecord
     self.table_name = :on_create
     has_paper_trail on: [:create]
   end
diff --git a/spec/dummy_app/app/models/on/destroy.rb b/spec/dummy_app/app/models/on/destroy.rb
index 6fe86d6..485e4be 100644
--- a/spec/dummy_app/app/models/on/destroy.rb
+++ b/spec/dummy_app/app/models/on/destroy.rb
@@ -1,7 +1,7 @@
 # frozen_string_literal: true
 
 module On
-  class Destroy < ActiveRecord::Base
+  class Destroy < ApplicationRecord
     self.table_name = :on_destroy
     has_paper_trail on: [:destroy]
   end
diff --git a/spec/dummy_app/app/models/on/empty_array.rb b/spec/dummy_app/app/models/on/empty_array.rb
index 0de30e8..1f8f1c5 100644
--- a/spec/dummy_app/app/models/on/empty_array.rb
+++ b/spec/dummy_app/app/models/on/empty_array.rb
@@ -1,7 +1,7 @@
 # frozen_string_literal: true
 
 module On
-  class EmptyArray < ActiveRecord::Base
+  class EmptyArray < ApplicationRecord
     self.table_name = :on_empty_array
     has_paper_trail on: []
   end
diff --git a/spec/dummy_app/app/models/on/touch.rb b/spec/dummy_app/app/models/on/touch.rb
index 37455c3..c7efd68 100644
--- a/spec/dummy_app/app/models/on/touch.rb
+++ b/spec/dummy_app/app/models/on/touch.rb
@@ -1,7 +1,7 @@
 # frozen_string_literal: true
 
 module On
-  class Touch < ActiveRecord::Base
+  class Touch < ApplicationRecord
     self.table_name = :on_touch
     has_paper_trail on: [:touch]
   end
diff --git a/spec/dummy_app/app/models/on/update.rb b/spec/dummy_app/app/models/on/update.rb
index 0c48f29..8571694 100644
--- a/spec/dummy_app/app/models/on/update.rb
+++ b/spec/dummy_app/app/models/on/update.rb
@@ -1,7 +1,7 @@
 # frozen_string_literal: true
 
 module On
-  class Update < ActiveRecord::Base
+  class Update < ApplicationRecord
     self.table_name = :on_update
     has_paper_trail on: [:update]
   end
diff --git a/spec/dummy_app/app/models/order.rb b/spec/dummy_app/app/models/order.rb
index f272874..84e5ab4 100644
--- a/spec/dummy_app/app/models/order.rb
+++ b/spec/dummy_app/app/models/order.rb
@@ -1,7 +1,7 @@
 # frozen_string_literal: true
 
-class Order < ActiveRecord::Base
-  belongs_to :customer
+class Order < ApplicationRecord
+  belongs_to :customer, touch: :touched_at
   has_many :line_items
   has_paper_trail
 end
diff --git a/spec/dummy_app/app/models/paragraph.rb b/spec/dummy_app/app/models/paragraph.rb
index b92c7b6..efb05eb 100644
--- a/spec/dummy_app/app/models/paragraph.rb
+++ b/spec/dummy_app/app/models/paragraph.rb
@@ -1,6 +1,6 @@
 # frozen_string_literal: true
 
-class Paragraph < ActiveRecord::Base
+class Paragraph < ApplicationRecord
   belongs_to :section
 
   has_paper_trail
diff --git a/spec/dummy_app/app/models/person.rb b/spec/dummy_app/app/models/person.rb
index 77dca21..fb1aa4b 100644
--- a/spec/dummy_app/app/models/person.rb
+++ b/spec/dummy_app/app/models/person.rb
@@ -1,6 +1,6 @@
 # frozen_string_literal: true
 
-class Person < ActiveRecord::Base
+class Person < ApplicationRecord
   has_many :authorships, foreign_key: :author_id, dependent: :destroy
   has_many :books, through: :authorships
 
@@ -14,7 +14,7 @@ class Person < ActiveRecord::Base
 
   has_one :thing
 
-  belongs_to :mentor, class_name: "Person", foreign_key: :mentor_id, optional: true
+  belongs_to :mentor, class_name: "Person", optional: true
 
   has_paper_trail
 
diff --git a/spec/dummy_app/app/models/pet.rb b/spec/dummy_app/app/models/pet.rb
index ee9944d..0d7c0b8 100644
--- a/spec/dummy_app/app/models/pet.rb
+++ b/spec/dummy_app/app/models/pet.rb
@@ -1,6 +1,6 @@
 # frozen_string_literal: true
 
-class Pet < ActiveRecord::Base
+class Pet < ApplicationRecord
   belongs_to :owner, class_name: "Person", optional: true
   belongs_to :animal, optional: true
   has_paper_trail
diff --git a/spec/dummy_app/app/models/plant.rb b/spec/dummy_app/app/models/plant.rb
new file mode 100644
index 0000000..e1d88b5
--- /dev/null
+++ b/spec/dummy_app/app/models/plant.rb
@@ -0,0 +1,22 @@
+# frozen_string_literal: true
+
+class Plant < ApplicationRecord
+  has_paper_trail
+  self.inheritance_column = "species"
+
+  class << self
+    # Rails 6.1 adds a public method to overwrite sti finder methods. In earlier versions, users
+    # may use the private method find_sti_class.
+    #
+    # See https://github.com/rails/rails/pull/37500
+    if method_defined?(:sti_class_for)
+      def sti_class_for(type_name)
+        super(type_name.camelize)
+      end
+    else
+      def find_sti_class(type_name)
+        super(type_name.camelize)
+      end
+    end
+  end
+end
diff --git a/spec/dummy_app/app/models/post.rb b/spec/dummy_app/app/models/post.rb
index 68871df..39c5738 100644
--- a/spec/dummy_app/app/models/post.rb
+++ b/spec/dummy_app/app/models/post.rb
@@ -1,5 +1,5 @@
 # frozen_string_literal: true
 
-class Post < ActiveRecord::Base
+class Post < ApplicationRecord
   has_paper_trail versions: { class_name: "PostVersion" }
 end
diff --git a/spec/dummy_app/app/models/post_with_status.rb b/spec/dummy_app/app/models/post_with_status.rb
index d9685f9..3a408ad 100644
--- a/spec/dummy_app/app/models/post_with_status.rb
+++ b/spec/dummy_app/app/models/post_with_status.rb
@@ -2,7 +2,7 @@
 
 # This model tests ActiveRecord::Enum, which was added in AR 4.1
 # http://edgeguides.rubyonrails.org/4_1_release_notes.html#active-record-enums
-class PostWithStatus < ActiveRecord::Base
+class PostWithStatus < ApplicationRecord
   has_paper_trail
 
   enum status: { draft: 0, published: 1, archived: 2 }
diff --git a/spec/dummy_app/app/models/postgres_user.rb b/spec/dummy_app/app/models/postgres_user.rb
index b6cfea8..c238ae3 100644
--- a/spec/dummy_app/app/models/postgres_user.rb
+++ b/spec/dummy_app/app/models/postgres_user.rb
@@ -1,5 +1,5 @@
 # frozen_string_literal: true
 
-class PostgresUser < ActiveRecord::Base
+class PostgresUser < ApplicationRecord
   has_paper_trail
 end
diff --git a/spec/dummy_app/app/models/quotation.rb b/spec/dummy_app/app/models/quotation.rb
index 1389228..f9ee95a 100644
--- a/spec/dummy_app/app/models/quotation.rb
+++ b/spec/dummy_app/app/models/quotation.rb
@@ -1,6 +1,6 @@
 # frozen_string_literal: true
 
-class Quotation < ActiveRecord::Base
+class Quotation < ApplicationRecord
   belongs_to :chapter
   has_many :citations, dependent: :destroy
   has_paper_trail
diff --git a/spec/dummy_app/app/models/section.rb b/spec/dummy_app/app/models/section.rb
index 81a1e56..424a71a 100644
--- a/spec/dummy_app/app/models/section.rb
+++ b/spec/dummy_app/app/models/section.rb
@@ -1,6 +1,6 @@
 # frozen_string_literal: true
 
-class Section < ActiveRecord::Base
+class Section < ApplicationRecord
   belongs_to :chapter
   has_many :paragraphs, dependent: :destroy
 
diff --git a/spec/dummy_app/app/models/skipper.rb b/spec/dummy_app/app/models/skipper.rb
index 852ed07..094558a 100644
--- a/spec/dummy_app/app/models/skipper.rb
+++ b/spec/dummy_app/app/models/skipper.rb
@@ -1,5 +1,5 @@
 # frozen_string_literal: true
 
-class Skipper < ActiveRecord::Base
+class Skipper < ApplicationRecord
   has_paper_trail ignore: [:created_at], skip: [:another_timestamp]
 end
diff --git a/spec/dummy_app/app/models/song.rb b/spec/dummy_app/app/models/song.rb
index 05f823d..3196689 100644
--- a/spec/dummy_app/app/models/song.rb
+++ b/spec/dummy_app/app/models/song.rb
@@ -1,6 +1,6 @@
 # frozen_string_literal: true
 
-class Song < ActiveRecord::Base
+class Song < ApplicationRecord
   has_paper_trail
   attribute :name, :string
 
diff --git a/spec/dummy_app/app/models/thing.rb b/spec/dummy_app/app/models/thing.rb
index 079b1b0..bfb39ec 100644
--- a/spec/dummy_app/app/models/thing.rb
+++ b/spec/dummy_app/app/models/thing.rb
@@ -1,6 +1,6 @@
 # frozen_string_literal: true
 
-class Thing < ActiveRecord::Base
+class Thing < ApplicationRecord
   has_paper_trail versions: {
     scope: -> { order("id desc") },
     extend: PrefixVersionsInspectWithCount
diff --git a/spec/dummy_app/app/models/tomato.rb b/spec/dummy_app/app/models/tomato.rb
new file mode 100644
index 0000000..42f3e0c
--- /dev/null
+++ b/spec/dummy_app/app/models/tomato.rb
@@ -0,0 +1,9 @@
+# frozen_string_literal: true
+
+class Tomato < Plant
+  class << self
+    def sti_name
+      "tomato"
+    end
+  end
+end
diff --git a/spec/dummy_app/app/models/translation.rb b/spec/dummy_app/app/models/translation.rb
index cdcc3b3..ec2d581 100644
--- a/spec/dummy_app/app/models/translation.rb
+++ b/spec/dummy_app/app/models/translation.rb
@@ -1,13 +1,9 @@
 # frozen_string_literal: true
 
 # Demonstrates the `if` and `unless` configuration options.
-class Translation < ActiveRecord::Base
-  # Has a `type` column, but it's not used for STI.
-  # TODO: rename column
-  self.inheritance_column = nil
-
+class Translation < ApplicationRecord
   has_paper_trail(
     if: proc { |t| t.language_code == "US" },
-    unless: proc { |t| t.type == "DRAFT" }
+    unless: proc { |t| t.draft_status == "DRAFT" }
   )
 end
diff --git a/spec/dummy_app/app/models/vegetable.rb b/spec/dummy_app/app/models/vegetable.rb
new file mode 100644
index 0000000..cca41cf
--- /dev/null
+++ b/spec/dummy_app/app/models/vegetable.rb
@@ -0,0 +1,8 @@
+# frozen_string_literal: true
+
+# See also `Fruit` which uses `JsonVersion`.
+class Vegetable < ApplicationRecord
+  has_paper_trail versions: {
+    class_name: ENV["DB"] == "postgres" ? "JsonbVersion" : "PaperTrail::Version"
+  }, on: %i[create update]
+end
diff --git a/spec/dummy_app/app/models/vehicle.rb b/spec/dummy_app/app/models/vehicle.rb
index 2ed1ba4..8268c24 100644
--- a/spec/dummy_app/app/models/vehicle.rb
+++ b/spec/dummy_app/app/models/vehicle.rb
@@ -1,6 +1,6 @@
 # frozen_string_literal: true
 
-class Vehicle < ActiveRecord::Base
+class Vehicle < ApplicationRecord
   # This STI parent class specifically does not call `has_paper_trail`.
   # Of its sub-classes, only `Car` and `Bicycle` are versioned.
   belongs_to :owner, class_name: "Person", optional: true
diff --git a/spec/dummy_app/app/models/whatchamajigger.rb b/spec/dummy_app/app/models/whatchamajigger.rb
index 25ccc9d..9f27f1b 100644
--- a/spec/dummy_app/app/models/whatchamajigger.rb
+++ b/spec/dummy_app/app/models/whatchamajigger.rb
@@ -1,6 +1,6 @@
 # frozen_string_literal: true
 
-class Whatchamajigger < ActiveRecord::Base
+class Whatchamajigger < ApplicationRecord
   has_paper_trail
   belongs_to :owner, polymorphic: true, optional: true
 end
diff --git a/spec/dummy_app/app/models/widget.rb b/spec/dummy_app/app/models/widget.rb
index 84b4021..488b1cf 100644
--- a/spec/dummy_app/app/models/widget.rb
+++ b/spec/dummy_app/app/models/widget.rb
@@ -1,6 +1,6 @@
 # frozen_string_literal: true
 
-class Widget < ActiveRecord::Base
+class Widget < ApplicationRecord
   EXCLUDED_NAME = "Biglet"
   has_paper_trail
   has_one :wotsit
diff --git a/spec/dummy_app/app/models/wotsit.rb b/spec/dummy_app/app/models/wotsit.rb
index cd4b9ad..3744553 100644
--- a/spec/dummy_app/app/models/wotsit.rb
+++ b/spec/dummy_app/app/models/wotsit.rb
@@ -1,6 +1,6 @@
 # frozen_string_literal: true
 
-class Wotsit < ActiveRecord::Base
+class Wotsit < ApplicationRecord
   has_paper_trail
 
   belongs_to :widget, optional: true
diff --git a/spec/dummy_app/app/versions/abstract_version.rb b/spec/dummy_app/app/versions/abstract_version.rb
index a55e07c..36e40d7 100644
--- a/spec/dummy_app/app/versions/abstract_version.rb
+++ b/spec/dummy_app/app/versions/abstract_version.rb
@@ -1,6 +1,6 @@
 # frozen_string_literal: true
 
-class AbstractVersion < ActiveRecord::Base
+class AbstractVersion < ApplicationRecord
   include PaperTrail::VersionConcern
   self.abstract_class = true
 end
diff --git a/spec/dummy_app/app/versions/jsonb_version.rb b/spec/dummy_app/app/versions/jsonb_version.rb
new file mode 100644
index 0000000..26e2a03
--- /dev/null
+++ b/spec/dummy_app/app/versions/jsonb_version.rb
@@ -0,0 +1,7 @@
+# frozen_string_literal: true
+
+class JsonbVersion < ApplicationRecord
+  include PaperTrail::VersionConcern
+
+  self.table_name = "jsonb_versions"
+end
diff --git a/spec/dummy_app/config/application.rb b/spec/dummy_app/config/application.rb
index 6f4de37..92d3a03 100644
--- a/spec/dummy_app/config/application.rb
+++ b/spec/dummy_app/config/application.rb
@@ -13,7 +13,17 @@ require File.expand_path("boot", __dir__)
 
 module Dummy
   class Application < Rails::Application
-    config.load_defaults(::Rails.gem_version.segments.take(2).join("."))
+    YAML_COLUMN_PERMITTED_CLASSES = [
+      ::ActiveRecord::Type::Time::Value,
+      ::ActiveSupport::TimeWithZone,
+      ::ActiveSupport::TimeZone,
+      ::BigDecimal,
+      ::Date,
+      ::Symbol,
+      ::Time
+    ].freeze
+
+    config.load_defaults(::ActiveRecord.gem_version.segments.take(2).join("."))
 
     config.encoding = "utf-8"
     config.filter_parameters += [:password]
@@ -21,11 +31,14 @@ module Dummy
     config.active_support.test_order = :sorted
     config.secret_key_base = "A fox regularly kicked the screaming pile of biscuits."
 
-    # In rails >= 6.0, "`.represent_boolean_as_integer=` is now always true,
-    # so setting this is deprecated and will be removed in Rails 6.1."
-    if ::ENV["DB"] == "sqlite" &&
-        ::Gem::Requirement.new("~> 5.2").satisfied_by?(::Rails.gem_version)
-      config.active_record.sqlite3.represent_boolean_as_integer = true
+    # `use_yaml_unsafe_load` was added in 5.2.8.1, 6.0.5.1, 6.1.6.1, and 7.0.3.1.
+    # Will be removed in 7.1.0?
+    if ::ActiveRecord.respond_to?(:use_yaml_unsafe_load) # 7.0.3.1
+      ::ActiveRecord.use_yaml_unsafe_load = false
+      ::ActiveRecord.yaml_column_permitted_classes = YAML_COLUMN_PERMITTED_CLASSES
+    elsif ::ActiveRecord::Base.respond_to?(:use_yaml_unsafe_load) # 5.2.8.1, 6.0.5.1, 6.1.6.1
+      ::ActiveRecord::Base.use_yaml_unsafe_load = false
+      ::ActiveRecord::Base.yaml_column_permitted_classes = YAML_COLUMN_PERMITTED_CLASSES
     end
   end
 end
diff --git a/spec/dummy_app/db/migrate/20110208155312_set_up_test_tables.rb b/spec/dummy_app/db/migrate/20110208155312_set_up_test_tables.rb
index 4a13ac2..a8b8058 100644
--- a/spec/dummy_app/db/migrate/20110208155312_set_up_test_tables.rb
+++ b/spec/dummy_app/db/migrate/20110208155312_set_up_test_tables.rb
@@ -128,19 +128,23 @@ class SetUpTestTables < ::ActiveRecord::Migration::Current
     add_index :no_object_versions, %i[item_type item_id]
 
     if ENV["DB"] == "postgres"
-      create_table :json_versions, force: true do |t|
-        t.string   :item_type, null: false
-        t.integer  :item_id,   null: false
-        t.string   :event,     null: false
-        t.string   :whodunnit
-        t.json     :object
-        t.json     :object_changes
-        t.datetime :created_at, limit: 6
+      %w[json jsonb].each do |j|
+        table_name = j + "_versions"
+        create_table table_name, force: true do |t|
+          t.string   :item_type, null: false
+          t.bigint   :item_id, null: false
+          t.string   :event, null: false
+          t.string   :whodunnit
+          t.public_send j, :object
+          t.public_send j, :object_changes
+          t.datetime :created_at, limit: 6
+        end
+        add_index table_name, %i[item_type item_id]
       end
-      add_index :json_versions, %i[item_type item_id]
     end
 
     create_table :not_on_updates, force: true do |t|
+      t.string :name
       t.timestamps null: true, limit: 6
     end
 
@@ -230,6 +234,10 @@ class SetUpTestTables < ::ActiveRecord::Migration::Current
       t.integer :animal_id
     end
 
+    create_table :plants, force: true do |t|
+      t.string :species # custom single table inheritance column
+    end
+
     create_table :documents, force: true do |t|
       t.string :name
     end
@@ -245,10 +253,10 @@ class SetUpTestTables < ::ActiveRecord::Migration::Current
     end
 
     create_table :translations, force: true do |t|
-      t.string    :headline
       t.string    :content
+      t.string    :draft_status
+      t.string    :headline
       t.string    :language_code
-      t.string    :type
     end
 
     create_table :gadgets, force: true do |t|
@@ -260,6 +268,7 @@ class SetUpTestTables < ::ActiveRecord::Migration::Current
 
     create_table :customers, force: true do |t|
       t.string :name
+      t.datetime :touched_at, limit: 6
     end
 
     create_table :orders, force: true do |t|
@@ -273,8 +282,9 @@ class SetUpTestTables < ::ActiveRecord::Migration::Current
     end
 
     create_table :fruits, force: true do |t|
-      t.string :name
       t.string :color
+      t.integer :mass
+      t.string :name
     end
 
     create_table :boolits, force: true do |t|
@@ -354,6 +364,12 @@ class SetUpTestTables < ::ActiveRecord::Migration::Current
       t.integer :parent_id
       t.integer :partner_id
     end
+
+    create_table :vegetables, force: true do |t|
+      t.string :color
+      t.integer :mass
+      t.string :name
+    end
   end
 
   def down
diff --git a/spec/generators/paper_trail/install_generator_spec.rb b/spec/generators/paper_trail/install_generator_spec.rb
index 10365b2..34f1386 100644
--- a/spec/generators/paper_trail/install_generator_spec.rb
+++ b/spec/generators/paper_trail/install_generator_spec.rb
@@ -33,9 +33,9 @@ RSpec.describe PaperTrail::InstallGenerator, type: :generator do
       }.call
       expected_item_type_options = lambda {
         if described_class::MYSQL_ADAPTERS.include?(ActiveRecord::Base.connection.class.name)
-          ", { null: false, limit: 191 }"
+          ", null: false, limit: 191"
         else
-          ", { null: false }"
+          ", null: false"
         end
       }.call
       expect(destination_root).to(
@@ -102,4 +102,26 @@ RSpec.describe PaperTrail::InstallGenerator, type: :generator do
       )
     end
   end
+
+  describe "`--uuid` option set to `true`" do
+    before do
+      prepare_destination
+      run_generator %w[--uuid]
+    end
+
+    it "generates a migration for creating the 'versions' table with item_id type uuid" do
+      expected_item_id_type = "string"
+      expect(destination_root).to(
+        have_structure {
+          directory("db") {
+            directory("migrate") {
+              migration("create_versions") {
+                contains "t.#{expected_item_id_type}   :item_id,   null: false"
+              }
+            }
+          }
+        }
+      )
+    end
+  end
 end
diff --git a/spec/models/animal_spec.rb b/spec/models/animal_spec.rb
index e1e1ed6..20b7485 100644
--- a/spec/models/animal_spec.rb
+++ b/spec/models/animal_spec.rb
@@ -4,8 +4,8 @@ require "spec_helper"
 
 RSpec.describe Animal, type: :model, versioning: true do
   it "baseline test setup" do
-    expect(Animal.new).to be_versioned
-    expect(Animal.inheritance_column).to eq("species")
+    expect(described_class.new).to be_versioned
+    expect(described_class.inheritance_column).to eq("species")
   end
 
   describe "#descends_from_active_record?" do
@@ -15,7 +15,7 @@ RSpec.describe Animal, type: :model, versioning: true do
   end
 
   it "works with custom STI inheritance column" do
-    animal = Animal.create(name: "Animal")
+    animal = described_class.create(name: "Animal")
     animal.update(name: "Animal from the Muppets")
     animal.update(name: "Animal Muppet")
     animal.destroy
@@ -46,7 +46,7 @@ RSpec.describe Animal, type: :model, versioning: true do
   it "allows the inheritance_column (species) to be updated" do
     cat = Cat.create!(name: "Leo")
     cat.update(name: "Spike", species: "Dog")
-    dog = Animal.find(cat.id)
+    dog = described_class.find(cat.id)
     expect(dog).to be_instance_of(Dog)
   end
 
diff --git a/spec/models/article_spec.rb b/spec/models/article_spec.rb
index 8526332..d8ec2b1 100644
--- a/spec/models/article_spec.rb
+++ b/spec/models/article_spec.rb
@@ -11,7 +11,7 @@ RSpec.describe Article, type: :model, versioning: true do
     end
   end
 
-  context "which updates an ignored column" do
+  context "when updating an ignored column" do
     it "not change the number of versions" do
       article = described_class.create
       article.update(title: "My first title")
@@ -19,7 +19,7 @@ RSpec.describe Article, type: :model, versioning: true do
     end
   end
 
-  context "which updates an ignored column with truly Proc" do
+  context "when updating an ignored column with truly Proc" do
     it "not change the number of versions" do
       article = described_class.create
       article.update(abstract: "ignore abstract")
@@ -27,7 +27,7 @@ RSpec.describe Article, type: :model, versioning: true do
     end
   end
 
-  context "which updates an ignored column with falsy Proc" do
+  context "when updating an ignored column with falsy Proc" do
     it "change the number of versions" do
       article = described_class.create
       article.update(abstract: "do not ignore abstract!")
@@ -35,7 +35,7 @@ RSpec.describe Article, type: :model, versioning: true do
     end
   end
 
-  context "which updates an ignored column, ignored with truly Proc and a selected column" do
+  context "when updating an ignored column, ignored with truly Proc and a selected column" do
     it "change the number of versions" do
       article = described_class.create
       article.update(
@@ -59,7 +59,7 @@ RSpec.describe Article, type: :model, versioning: true do
     end
   end
 
-  context "which updates an ignored column, ignored with falsy Proc and a selected column" do
+  context "when updating an ignored column, ignored with falsy Proc and a selected column" do
     it "change the number of versions" do
       article = described_class.create
       article.update(
@@ -86,7 +86,7 @@ RSpec.describe Article, type: :model, versioning: true do
     end
   end
 
-  context "which updates a selected column" do
+  context "when updating a selected column" do
     it "change the number of versions" do
       article = described_class.create
       article.update(content: "Some text here.")
@@ -95,7 +95,7 @@ RSpec.describe Article, type: :model, versioning: true do
     end
   end
 
-  context "which updates a non-ignored and non-selected column" do
+  context "when updating a non-ignored and non-selected column" do
     it "not change the number of versions" do
       article = described_class.create
       article.update(abstract: "Other abstract")
@@ -103,7 +103,7 @@ RSpec.describe Article, type: :model, versioning: true do
     end
   end
 
-  context "which updates a skipped column" do
+  context "when updating a skipped column" do
     it "not change the number of versions" do
       article = described_class.create
       article.update(file_upload: "Your data goes here")
@@ -111,7 +111,7 @@ RSpec.describe Article, type: :model, versioning: true do
     end
   end
 
-  context "which updates a skipped column and a selected column" do
+  context "when updating a skipped column and a selected column" do
     it "change the number of versions" do
       article = described_class.create
       article.update(
@@ -141,7 +141,7 @@ RSpec.describe Article, type: :model, versioning: true do
       ).to(eq("content" => [nil, "Some text here."]))
     end
 
-    context "and when updated again" do
+    context "when updated again" do
       it "have removed the skipped attributes when saving the previous version" do
         article = described_class.create
         article.update(
@@ -185,4 +185,79 @@ RSpec.describe Article, type: :model, versioning: true do
       expect(article.versions.map(&:event)).to(match_array(%w[create destroy]))
     end
   end
+
+  context "with an item" do
+    let(:article) { described_class.new(title: initial_title) }
+    let(:initial_title) { "Foobar" }
+
+    context "when it is created" do
+      before { article.save }
+
+      it "store fixed meta data" do
+        expect(article.versions.last.answer).to(eq(42))
+      end
+
+      it "store dynamic meta data which is independent of the item" do
+        expect(article.versions.last.question).to(eq("31 + 11 = 42"))
+      end
+
+      it "store dynamic meta data which depends on the item" do
+        expect(article.versions.last.article_id).to(eq(article.id))
+      end
+
+      it "store dynamic meta data based on a method of the item" do
+        expect(article.versions.last.action).to(eq(article.action_data_provider_method))
+      end
+
+      it "store dynamic meta data based on an attribute of the item at creation" do
+        expect(article.versions.last.title).to(eq(initial_title))
+      end
+    end
+
+    context "when it is created, then updated" do
+      before do
+        article.save
+        article.update!(content: "Better text.", title: "Rhubarb")
+      end
+
+      it "store fixed meta data" do
+        expect(article.versions.last.answer).to(eq(42))
+      end
+
+      it "store dynamic meta data which is independent of the item" do
+        expect(article.versions.last.question).to(eq("31 + 11 = 42"))
+      end
+
+      it "store dynamic meta data which depends on the item" do
+        expect(article.versions.last.article_id).to(eq(article.id))
+      end
+
+      it "store dynamic meta data based on an attribute of the item prior to the update" do
+        expect(article.versions.last.title).to(eq(initial_title))
+      end
+    end
+
+    context "when it is created, then destroyed" do
+      before do
+        article.save
+        article.destroy
+      end
+
+      it "store fixed metadata" do
+        expect(article.versions.last.answer).to(eq(42))
+      end
+
+      it "store dynamic metadata which is independent of the item" do
+        expect(article.versions.last.question).to(eq("31 + 11 = 42"))
+      end
+
+      it "store dynamic metadata which depends on the item" do
+        expect(article.versions.last.article_id).to(eq(article.id))
+      end
+
+      it "store dynamic metadata based on attribute of item prior to destruction" do
+        expect(article.versions.last.title).to(eq(initial_title))
+      end
+    end
+  end
 end
diff --git a/spec/models/book_spec.rb b/spec/models/book_spec.rb
new file mode 100644
index 0000000..d51f15f
--- /dev/null
+++ b/spec/models/book_spec.rb
@@ -0,0 +1,69 @@
+# frozen_string_literal: true
+
+require "spec_helper"
+
+RSpec.describe Book, versioning: true do
+  context "with :has_many :through" do
+    it "store version on source <<" do
+      book = described_class.create(title: "War and Peace")
+      dostoyevsky = Person.create(name: "Dostoyevsky")
+      Person.create(name: "Solzhenitsyn")
+      count = PaperTrail::Version.count
+      (book.authors << dostoyevsky)
+      expect((PaperTrail::Version.count - count)).to(eq(1))
+      expect(book.authorships.first.versions.first).to(eq(PaperTrail::Version.last))
+    end
+
+    it "store version on source create" do
+      book = described_class.create(title: "War and Peace")
+      Person.create(name: "Dostoyevsky")
+      Person.create(name: "Solzhenitsyn")
+      count = PaperTrail::Version.count
+      book.authors.create(name: "Tolstoy")
+      expect((PaperTrail::Version.count - count)).to(eq(2))
+      expect(
+        [PaperTrail::Version.order(:id).to_a[-2].item, PaperTrail::Version.last.item]
+      ).to match_array([Person.last, Authorship.last])
+    end
+
+    it "store version on join destroy" do
+      book = described_class.create(title: "War and Peace")
+      dostoyevsky = Person.create(name: "Dostoyevsky")
+      Person.create(name: "Solzhenitsyn")
+      (book.authors << dostoyevsky)
+      count = PaperTrail::Version.count
+      book.authorships.reload.last.destroy
+      expect((PaperTrail::Version.count - count)).to(eq(1))
+      expect(PaperTrail::Version.last.reify.book).to(eq(book))
+      expect(PaperTrail::Version.last.reify.author).to(eq(dostoyevsky))
+    end
+
+    it "store version on join clear" do
+      book = described_class.create(title: "War and Peace")
+      dostoyevsky = Person.create(name: "Dostoyevsky")
+      Person.create(name: "Solzhenitsyn")
+      book.authors << dostoyevsky
+      count = PaperTrail::Version.count
+      book.authorships.reload.destroy_all
+      expect((PaperTrail::Version.count - count)).to(eq(1))
+      expect(PaperTrail::Version.last.reify.book).to(eq(book))
+      expect(PaperTrail::Version.last.reify.author).to(eq(dostoyevsky))
+    end
+  end
+
+  context "when a persisted record is updated then destroyed" do
+    it "has changes" do
+      book = described_class.create! title: "A"
+      changes = YAML.load book.versions.last.attributes["object_changes"]
+      expect(changes).to eq("id" => [nil, book.id], "title" => [nil, "A"])
+
+      book.update! title: "B"
+      changes = YAML.load book.versions.last.attributes["object_changes"]
+      expect(changes).to eq("title" => %w[A B])
+
+      book.destroy
+      changes = YAML.load book.versions.last.attributes["object_changes"]
+      expect(changes).to eq("id" => [book.id, nil], "title" => ["B", nil])
+    end
+  end
+end
diff --git a/spec/models/boolit_spec.rb b/spec/models/boolit_spec.rb
index 58a4547..c022bce 100644
--- a/spec/models/boolit_spec.rb
+++ b/spec/models/boolit_spec.rb
@@ -4,7 +4,7 @@ require "spec_helper"
 require "support/custom_json_serializer"
 
 RSpec.describe Boolit, type: :model, versioning: true do
-  let(:boolit) { Boolit.create! }
+  let(:boolit) { described_class.create! }
 
   before { boolit.update!(name: FFaker::Name.name) }
 
@@ -16,11 +16,11 @@ RSpec.describe Boolit, type: :model, versioning: true do
     expect { boolit.versions.last.reify.save! }.not_to raise_error
   end
 
-  context "Instance falls out of default scope" do
+  context "when Instance falls out of default scope" do
     before { boolit.update!(scoped: false) }
 
     it "is NOT scoped" do
-      expect(Boolit.first).to be_nil
+      expect(described_class.first).to be_nil
     end
 
     it "still can be reified and persisted" do
diff --git a/spec/models/car_spec.rb b/spec/models/car_spec.rb
index cf0e1d9..37f98c8 100644
--- a/spec/models/car_spec.rb
+++ b/spec/models/car_spec.rb
@@ -7,9 +7,28 @@ RSpec.describe Car, type: :model do
 
   describe "changeset", versioning: true do
     it "has the expected keys (see issue 738)" do
-      car = Car.create!(name: "Alice")
+      car = described_class.create!(name: "Alice")
       car.update(name: "Bob")
       assert_includes car.versions.last.changeset.keys, "name"
     end
   end
+
+  describe "attributes and accessors", versioning: true do
+    it "reifies attributes that are not AR attributes" do
+      car = described_class.create name: "Pinto", color: "green"
+      car.update color: "yellow"
+      car.update color: "brown"
+      expect(car.versions.second.reify.color).to eq("yellow")
+    end
+
+    it "reifies attributes that once were attributes but now just attr_accessor" do
+      car = described_class.create name: "Pinto", color: "green"
+      car.update color: "yellow"
+      changes = PaperTrail::Serializers::YAML.load(car.versions.last.attributes["object"])
+      changes[:top_speed] = 80
+      car.versions.first.update object: changes.to_yaml
+      car.reload
+      expect(car.versions.first.reify.top_speed).to eq(80)
+    end
+  end
 end
diff --git a/spec/models/custom_primary_key_record_spec.rb b/spec/models/custom_primary_key_record_spec.rb
index 519f0ff..1c1a125 100644
--- a/spec/models/custom_primary_key_record_spec.rb
+++ b/spec/models/custom_primary_key_record_spec.rb
@@ -12,9 +12,9 @@ RSpec.describe CustomPrimaryKeyRecord, type: :model do
       version = custom_primary_key_record.versions.last
       expect(version).to be_a(CustomPrimaryKeyRecordVersion)
       version_from_db = CustomPrimaryKeyRecordVersion.last
-      expect(version_from_db.reify).to be_a(CustomPrimaryKeyRecord)
+      expect(version_from_db.reify).to be_a(described_class)
       custom_primary_key_record.destroy
-      expect(CustomPrimaryKeyRecordVersion.last.reify).to be_a(CustomPrimaryKeyRecord)
+      expect(CustomPrimaryKeyRecordVersion.last.reify).to be_a(described_class)
     end
   end
 end
diff --git a/spec/models/document_spec.rb b/spec/models/document_spec.rb
index 59e0a53..19bbe80 100644
--- a/spec/models/document_spec.rb
+++ b/spec/models/document_spec.rb
@@ -5,7 +5,7 @@ require "spec_helper"
 RSpec.describe Document, type: :model, versioning: true do
   describe "have_a_version_with matcher" do
     it "works with custom versions association" do
-      document = Document.create!(name: "Foo")
+      document = described_class.create!(name: "Foo")
       document.update!(name: "Bar")
       expect(document).to have_a_version_with(name: "Foo")
     end
@@ -13,7 +13,7 @@ RSpec.describe Document, type: :model, versioning: true do
 
   describe "#paper_trail.next_version" do
     it "returns the expected document" do
-      doc = Document.create
+      doc = described_class.create
       doc.update(name: "Doc 1")
       reified = doc.paper_trail_versions.last.reify
       expect(doc.name).to(eq(reified.paper_trail.next_version.name))
@@ -22,7 +22,7 @@ RSpec.describe Document, type: :model, versioning: true do
 
   describe "#paper_trail.previous_version" do
     it "returns the expected document" do
-      doc = Document.create
+      doc = described_class.create
       doc.update(name: "Doc 1")
       doc.update(name: "Doc 2")
       expect(doc.paper_trail_versions.length).to(eq(3))
@@ -32,7 +32,7 @@ RSpec.describe Document, type: :model, versioning: true do
 
   describe "#paper_trail_versions" do
     it "returns the expected version records" do
-      doc = Document.create
+      doc = described_class.create
       doc.update(name: "Doc 1")
       expect(doc.paper_trail_versions.length).to(eq(2))
       expect(doc.paper_trail_versions.map(&:event)).to(
@@ -43,7 +43,7 @@ RSpec.describe Document, type: :model, versioning: true do
 
   describe "#versions" do
     it "does not respond to versions method" do
-      doc = Document.create
+      doc = described_class.create
       doc.update(name: "Doc 1")
       expect(doc).not_to respond_to(:versions)
     end
diff --git a/spec/models/family/celebrity_family_spec.rb b/spec/models/family/celebrity_family_spec.rb
index b55e70b..f7c70aa 100644
--- a/spec/models/family/celebrity_family_spec.rb
+++ b/spec/models/family/celebrity_family_spec.rb
@@ -29,7 +29,7 @@ module Family
     end
 
     describe "#reify" do
-      context "belongs_to" do
+      context "with belongs_to" do
         it "uses the correct item_subtype" do
           parent = described_class.new(name: "Jermaine Jackson")
           parent.path_to_stardom = "Emulating Motown greats such as the Temptations and "\
@@ -51,7 +51,7 @@ module Family
         end
       end
 
-      context "has_many" do
+      context "with has_many" do
         it "uses the correct item_type in queries" do
           parent = described_class.new(name: "Gomez Addams")
           parent.path_to_stardom = "Buy a Victorian house next to a sprawling graveyard, "\
@@ -70,7 +70,7 @@ module Family
         end
       end
 
-      context "has_many through" do
+      context "with has_many through" do
         it "uses the correct item_type in queries" do
           parent = described_class.new(name: "Grandad")
           parent.path_to_stardom = "Took a suitcase and started running a market trading "\
@@ -90,7 +90,7 @@ module Family
         end
       end
 
-      context "has_one" do
+      context "with has_one" do
         it "uses the correct item_type in queries" do
           parent = described_class.new(name: "Minnie Marx")
           parent.path_to_stardom = "Gain a relentless dedication to the stage by having a "\
diff --git a/spec/models/foo_widget_spec.rb b/spec/models/foo_widget_spec.rb
new file mode 100644
index 0000000..8e3bbfc
--- /dev/null
+++ b/spec/models/foo_widget_spec.rb
@@ -0,0 +1,35 @@
+# frozen_string_literal: true
+
+require "spec_helper"
+require "support/performance_helpers"
+
+RSpec.describe(FooWidget, versioning: true) do
+  context "with a subclass" do
+    let(:foo) { described_class.create }
+
+    before do
+      foo.update!(name: "Foo")
+    end
+
+    it "reify with the correct type" do
+      expect(PaperTrail::Version.last.previous).to(eq(foo.versions.first))
+      expect(PaperTrail::Version.last.next).to(be_nil)
+    end
+
+    it "returns the correct originator" do
+      PaperTrail.request.whodunnit = "Ben"
+      foo.update_attribute(:name, "Geoffrey")
+      expect(foo.paper_trail.originator).to(eq(PaperTrail.request.whodunnit))
+    end
+
+    context "when destroyed" do
+      before { foo.destroy }
+
+      it "reify with the correct type" do
+        assert_kind_of(described_class, foo.versions.last.reify)
+        expect(PaperTrail::Version.last.previous).to(eq(foo.versions[1]))
+        expect(PaperTrail::Version.last.next).to(be_nil)
+      end
+    end
+  end
+end
diff --git a/spec/models/fruit_spec.rb b/spec/models/fruit_spec.rb
index acb70fc..10e12bc 100644
--- a/spec/models/fruit_spec.rb
+++ b/spec/models/fruit_spec.rb
@@ -2,19 +2,54 @@
 
 require "spec_helper"
 
-if ENV["DB"] == "postgres" || JsonVersion.table_exists?
+if ENV["DB"] == "postgres" && JsonVersion.table_exists?
   RSpec.describe Fruit, type: :model, versioning: true do
     describe "have_a_version_with_changes matcher" do
       it "works with Fruit because Fruit uses JsonVersion" do
         # As of PT 9.0.0, with_version_changes only supports json(b) columns,
         # so that's why were testing the have_a_version_with_changes matcher
         # here.
-        banana = Fruit.create!(color: "Red", name: "Banana")
+        banana = described_class.create!(color: "Red", name: "Banana")
         banana.update!(color: "Yellow")
         expect(banana).to have_a_version_with_changes(color: "Yellow")
         expect(banana).not_to have_a_version_with_changes(color: "Pink")
         expect(banana).not_to have_a_version_with_changes(color: "Yellow", name: "Kiwi")
       end
     end
+
+    describe "queries of versions", versioning: true do
+      let!(:fruit) { described_class.create(name: "Apple", mass: 1, color: "green") }
+
+      before do
+        described_class.create(name: "Pear")
+        fruit.update(name: "Fidget")
+        fruit.update(name: "Digit")
+      end
+
+      it "return the fruit whose name has changed" do
+        result = JsonVersion.where_attribute_changes(:name).map(&:item)
+        expect(result).to include(fruit)
+      end
+
+      it "returns the fruit whose name was Fidget" do
+        result = JsonVersion.where_object_changes_from({ name: "Fidget" }).map(&:item)
+        expect(result).to include(fruit)
+      end
+
+      it "returns the fruit whose name became Digit" do
+        result = JsonVersion.where_object_changes_to({ name: "Digit" }).map(&:item)
+        expect(result).to include(fruit)
+      end
+
+      it "returns the fruit where the object was named Fidget before it changed" do
+        result = JsonVersion.where_object({ name: "Fidget" }).map(&:item)
+        expect(result).to include(fruit)
+      end
+
+      it "returns the fruit that changed to Fidget" do
+        result = JsonVersion.where_object_changes({ name: "Fidget" }).map(&:item)
+        expect(result).to include(fruit)
+      end
+    end
   end
 end
diff --git a/spec/models/gadget_spec.rb b/spec/models/gadget_spec.rb
index 531f116..5c8bd06 100644
--- a/spec/models/gadget_spec.rb
+++ b/spec/models/gadget_spec.rb
@@ -3,7 +3,7 @@
 require "spec_helper"
 
 RSpec.describe Gadget, type: :model do
-  let(:gadget) { Gadget.create!(name: "Wrench", brand: "Acme") }
+  let(:gadget) { described_class.create!(name: "Wrench", brand: "Acme") }
 
   it { is_expected.to be_versioned }
 
@@ -12,13 +12,13 @@ RSpec.describe Gadget, type: :model do
       expect { gadget.update_attribute(:name, "Hammer") }.to(change { gadget.versions.size }.by(1))
     end
 
-    context "ignored via symbol" do
+    context "when ignored via symbol" do
       it "doesn't generate a version" do
         expect { gadget.update_attribute(:brand, "Picard") }.not_to(change { gadget.versions.size })
       end
     end
 
-    context "ignored via Hash" do
+    context "when ignored via Hash" do
       it "generates a version when the ignored attribute isn't true" do
         expect { gadget.update_attribute(:color, "Blue") }.to(change { gadget.versions.size }.by(1))
         expect(gadget.versions.last.changeset.keys).to eq %w[color updated_at]
@@ -35,7 +35,11 @@ RSpec.describe Gadget, type: :model do
         gadget.update_attribute(:updated_at, Time.current + 1)
       }.to(change { gadget.versions.size }.by(1))
       expect(
-        YAML.load(gadget.versions.last.object_changes).keys
+        if ::YAML.respond_to?(:unsafe_load)
+          YAML.unsafe_load(gadget.versions.last.object_changes).keys
+        else
+          YAML.load(gadget.versions.last.object_changes).keys
+        end
       ).to eq(["updated_at"])
     end
   end
diff --git a/spec/models/joined_version_spec.rb b/spec/models/joined_version_spec.rb
index d144cfe..8046f1d 100644
--- a/spec/models/joined_version_spec.rb
+++ b/spec/models/joined_version_spec.rb
@@ -4,10 +4,10 @@ require "spec_helper"
 
 RSpec.describe JoinedVersion, type: :model, versioning: true do
   let(:widget) { Widget.create!(name: FFaker::Name.name) }
-  let(:version) { JoinedVersion.first }
+  let(:version) { described_class.first }
 
   describe "default_scope" do
-    it { expect(JoinedVersion.default_scopes).not_to be_empty }
+    it { expect(described_class.default_scopes).not_to be_empty }
   end
 
   describe "VersionConcern::ClassMethods" do
@@ -15,19 +15,19 @@ RSpec.describe JoinedVersion, type: :model, versioning: true do
 
     describe "#subsequent" do
       it "does not raise error when there is a default_scope that joins" do
-        JoinedVersion.subsequent(version).first
+        described_class.subsequent(version).first
       end
     end
 
     describe "#preceding" do
       it "does not raise error when there is a default scope that joins" do
-        JoinedVersion.preceding(version).first
+        described_class.preceding(version).first
       end
     end
 
     describe "#between" do
       it "does not raise error when there is a default scope that joins" do
-        JoinedVersion.between(Time.current, 1.minute.from_now).first
+        described_class.between(Time.current, 1.minute.from_now).first
       end
     end
   end
diff --git a/spec/models/json_version_spec.rb b/spec/models/json_version_spec.rb
index d1d7253..72400e3 100644
--- a/spec/models/json_version_spec.rb
+++ b/spec/models/json_version_spec.rb
@@ -5,7 +5,7 @@ require "spec_helper"
 # The `json_versions` table tests postgres' `json` data type. So, that
 # table is only created when testing against postgres.
 if JsonVersion.table_exists?
-  RSpec.describe JsonVersion, type: :model do
+  RSpec.describe JsonVersion, type: :model, versioning: true do
     it "includes the VersionConcern module" do
       expect(described_class).to include(PaperTrail::VersionConcern)
     end
@@ -23,14 +23,14 @@ if JsonVersion.table_exists?
         ).to eq(0)
       end
 
-      context "invalid arguments" do
+      context "with invalid arguments" do
         it "raises an error" do
           expect { described_class.where_object(:foo) }.to raise_error(ArgumentError)
           expect { described_class.where_object([]) }.to raise_error(ArgumentError)
         end
       end
 
-      context "valid arguments", versioning: true do
+      context "with valid arguments", versioning: true do
         it "locates versions according to their `object` contents" do
           fruit = Fruit.create!(name: "apple")
           expect(fruit.versions.length).to eq(1)
@@ -65,14 +65,14 @@ if JsonVersion.table_exists?
         ).to eq(0)
       end
 
-      context "invalid arguments" do
+      context "with invalid arguments" do
         it "raises an error" do
           expect { described_class.where_object_changes(:foo) }.to raise_error(ArgumentError)
           expect { described_class.where_object_changes([]) }.to raise_error(ArgumentError)
         end
       end
 
-      context "valid arguments", versioning: true do
+      context "with valid arguments", versioning: true do
         it "finds versions according to their `object_changes` contents" do
           fruit = Fruit.create!(name: "apple")
           fruit.update!(name: "banana", color: "red")
diff --git a/spec/models/no_object_spec.rb b/spec/models/no_object_spec.rb
index 4c4e9e7..95f6249 100644
--- a/spec/models/no_object_spec.rb
+++ b/spec/models/no_object_spec.rb
@@ -27,7 +27,11 @@ RSpec.describe NoObject, versioning: true do
 
     # New feature: destroy populates object_changes
     # https://github.com/paper-trail-gem/paper_trail/pull/1123
-    h = YAML.load a["object_changes"]
+    h = if ::YAML.respond_to?(:unsafe_load)
+          YAML.unsafe_load a["object_changes"]
+        else
+          YAML.load a["object_changes"]
+        end
     expect(h["id"]).to eq([n.id, nil])
     expect(h["letter"]).to eq([n.letter, nil])
     expect(h["created_at"][0]).to be_present
@@ -38,12 +42,12 @@ RSpec.describe NoObject, versioning: true do
 
   describe "reify" do
     it "raises error" do
-      n = NoObject.create!(letter: "A")
+      n = described_class.create!(letter: "A")
       v = n.versions.last
       expect { v.reify }.to(
         raise_error(
-          ::RuntimeError,
-          "reify can't be called without an object column"
+          ::PaperTrail::Error,
+          "reify requires an object column"
         )
       )
     end
@@ -51,13 +55,13 @@ RSpec.describe NoObject, versioning: true do
 
   describe "where_object" do
     it "raises error" do
-      n = NoObject.create!(letter: "A")
+      n = described_class.create!(letter: "A")
       expect {
         n.versions.where_object(foo: "bar")
       }.to(
         raise_error(
-          ::RuntimeError,
-          "where_object can't be called without an object column"
+          ::PaperTrail::Error,
+          "where_object requires an object column"
         )
       )
     end
diff --git a/spec/models/not_on_update_spec.rb b/spec/models/not_on_update_spec.rb
index 2ab15d7..c811c56 100644
--- a/spec/models/not_on_update_spec.rb
+++ b/spec/models/not_on_update_spec.rb
@@ -11,5 +11,12 @@ RSpec.describe NotOnUpdate, type: :model do
         PaperTrail::Version.count
       }.by(+1)
     end
+
+    it "captures changes when in_after_callback is true" do
+      record.name = "test"
+      record.paper_trail.save_with_version(in_after_callback: true)
+      changeset = record.versions.last.changeset
+      expect(changeset[:name]).to eq([nil, "test"])
+    end
   end
 end
diff --git a/spec/models/order_spec.rb b/spec/models/order_spec.rb
new file mode 100644
index 0000000..99665ac
--- /dev/null
+++ b/spec/models/order_spec.rb
@@ -0,0 +1,15 @@
+# frozen_string_literal: true
+
+require "spec_helper"
+
+RSpec.describe Order, type: :model, versioning: true do
+  context "when the record destroyed" do
+    it "creates a version record for association" do
+      customer = Customer.create!
+      described_class.create!(customer: customer)
+      described_class.destroy_all
+
+      expect(customer.versions.count).to(eq(3))
+    end
+  end
+end
diff --git a/spec/models/person_spec.rb b/spec/models/person_spec.rb
index df47fa1..bd44f19 100644
--- a/spec/models/person_spec.rb
+++ b/spec/models/person_spec.rb
@@ -9,21 +9,21 @@ require "spec_helper"
 RSpec.describe Person, type: :model, versioning: true do
   describe "#time_zone" do
     it "returns an ActiveSupport::TimeZone" do
-      person = Person.new(time_zone: "Samoa")
+      person = described_class.new(time_zone: "Samoa")
       expect(person.time_zone.class).to(eq(ActiveSupport::TimeZone))
     end
   end
 
   context "when the model is saved" do
     it "version.object_changes should store long serialization of TimeZone object" do
-      person = Person.new(time_zone: "Samoa")
+      person = described_class.new(time_zone: "Samoa")
       person.save!
       len = person.versions.last.object_changes.length
       expect((len < 105)).to(be_truthy)
     end
 
     it "version.object_changes attribute should have stored the value from serializer" do
-      person = Person.new(time_zone: "Samoa")
+      person = described_class.new(time_zone: "Samoa")
       person.save!
       as_stored_in_version = HashWithIndifferentAccess[
         YAML.load(person.versions.last.object_changes)
@@ -34,14 +34,14 @@ RSpec.describe Person, type: :model, versioning: true do
     end
 
     it "version.changeset should convert attribute to original, unserialized value" do
-      person = Person.new(time_zone: "Samoa")
+      person = described_class.new(time_zone: "Samoa")
       person.save!
       unserialized_value = Person::TimeZoneSerializer.load(person.time_zone)
       expect(person.versions.last.changeset[:time_zone].last).to(eq(unserialized_value))
     end
 
     it "record.changes (before save) returns the original, unserialized values" do
-      person = Person.new(time_zone: "Samoa")
+      person = described_class.new(time_zone: "Samoa")
       changes_before_save = person.changes.dup
       person.save!
       expect(
@@ -50,7 +50,7 @@ RSpec.describe Person, type: :model, versioning: true do
     end
 
     it "version.changeset should be the same as record.changes was before the save" do
-      person = Person.new(time_zone: "Samoa")
+      person = described_class.new(time_zone: "Samoa")
       changes_before_save = person.changes.dup
       person.save!
       actual = person.versions.last.changeset.delete_if { |k, _v| (k.to_sym == :id) }
@@ -61,7 +61,7 @@ RSpec.describe Person, type: :model, versioning: true do
 
     context "when that attribute is updated" do
       it "object should not store long serialization of TimeZone object" do
-        person = Person.new(time_zone: "Samoa")
+        person = described_class.new(time_zone: "Samoa")
         person.save!
         person.assign_attributes(time_zone: "Pacific Time (US & Canada)")
         person.save!
@@ -70,7 +70,7 @@ RSpec.describe Person, type: :model, versioning: true do
       end
 
       it "object_changes should not store long serialization of TimeZone object" do
-        person = Person.new(time_zone: "Samoa")
+        person = described_class.new(time_zone: "Samoa")
         person.save!
         person.assign_attributes(time_zone: "Pacific Time (US & Canada)")
         person.save!
@@ -79,7 +79,7 @@ RSpec.describe Person, type: :model, versioning: true do
       end
 
       it "version.object attribute should have stored value from serializer" do
-        person = Person.new(time_zone: "Samoa")
+        person = described_class.new(time_zone: "Samoa")
         person.save!
         attribute_value_before_change = person.time_zone
         person.assign_attributes(time_zone: "Pacific Time (US & Canada)")
@@ -93,7 +93,7 @@ RSpec.describe Person, type: :model, versioning: true do
       end
 
       it "version.object_changes attribute should have stored value from serializer" do
-        person = Person.new(time_zone: "Samoa")
+        person = described_class.new(time_zone: "Samoa")
         person.save!
         person.assign_attributes(time_zone: "Pacific Time (US & Canada)")
         person.save!
@@ -106,7 +106,7 @@ RSpec.describe Person, type: :model, versioning: true do
       end
 
       it "version.reify should convert attribute to original, unserialized value" do
-        person = Person.new(time_zone: "Samoa")
+        person = described_class.new(time_zone: "Samoa")
         person.save!
         attribute_value_before_change = person.time_zone
         person.assign_attributes(time_zone: "Pacific Time (US & Canada)")
@@ -116,7 +116,7 @@ RSpec.describe Person, type: :model, versioning: true do
       end
 
       it "version.changeset should convert attribute to original, unserialized value" do
-        person = Person.new(time_zone: "Samoa")
+        person = described_class.new(time_zone: "Samoa")
         person.save!
         person.assign_attributes(time_zone: "Pacific Time (US & Canada)")
         person.save!
@@ -125,7 +125,7 @@ RSpec.describe Person, type: :model, versioning: true do
       end
 
       it "record.changes (before save) returns the original, unserialized values" do
-        person = Person.new(time_zone: "Samoa")
+        person = described_class.new(time_zone: "Samoa")
         person.save!
         person.assign_attributes(time_zone: "Pacific Time (US & Canada)")
         changes_before_save = person.changes.dup
@@ -136,7 +136,7 @@ RSpec.describe Person, type: :model, versioning: true do
       end
 
       it "version.changeset should be the same as record.changes was before the save" do
-        person = Person.new(time_zone: "Samoa")
+        person = described_class.new(time_zone: "Samoa")
         person.save!
         person.assign_attributes(time_zone: "Pacific Time (US & Canada)")
         changes_before_save = person.changes.dup
@@ -151,7 +151,7 @@ RSpec.describe Person, type: :model, versioning: true do
 
   describe "#cars and bicycles" do
     it "can be reified" do
-      person = Person.create(name: "Frank")
+      person = described_class.create(name: "Frank")
       car = Car.create(name: "BMW 325")
       bicycle = Bicycle.create(name: "BMX 1.0")
 
diff --git a/spec/models/pet_spec.rb b/spec/models/pet_spec.rb
index 6a991a0..061e86b 100644
--- a/spec/models/pet_spec.rb
+++ b/spec/models/pet_spec.rb
@@ -5,7 +5,7 @@ require "rails/generators"
 
 RSpec.describe Pet, type: :model, versioning: true do
   it "baseline test setup" do
-    expect(Pet.new).to be_versioned
+    expect(described_class.new).to be_versioned
   end
 
   it "can be reified" do
@@ -13,8 +13,8 @@ RSpec.describe Pet, type: :model, versioning: true do
     dog = Dog.create(name: "Snoopy")
     cat = Cat.create(name: "Garfield")
 
-    person.pets << Pet.create(animal: dog)
-    person.pets << Pet.create(animal: cat)
+    person.pets << described_class.create(animal: dog)
+    person.pets << described_class.create(animal: cat)
     person.update(name: "Steve")
 
     dog.update(name: "Beethoven")
@@ -24,7 +24,7 @@ RSpec.describe Pet, type: :model, versioning: true do
     expect(person.reload.versions.length).to(eq(3))
   end
 
-  context "Older version entry present where item_type refers to the base_class" do
+  context "when an older version entry's item_type refers to the base_class" do
     let(:cat) { Cat.create(name: "Garfield") }   # Index 0
     let(:animal) { Animal.create }               # Index 4
 
@@ -83,7 +83,7 @@ RSpec.describe Pet, type: :model, versioning: true do
 
     # After creating a bunch of records above, we change the inheritance_column
     # so that we can demonstrate passing hints to the migration generator.
-    context "simulate a historical change to inheritance_column" do
+    context "when there was a historical change to inheritance_column" do
       before do
         Animal.inheritance_column = "species_xyz"
       end
diff --git a/spec/models/plant_spec.rb b/spec/models/plant_spec.rb
new file mode 100644
index 0000000..4656b1f
--- /dev/null
+++ b/spec/models/plant_spec.rb
@@ -0,0 +1,30 @@
+# frozen_string_literal: true
+
+require "spec_helper"
+
+RSpec.describe Plant, type: :model, versioning: true do
+  it "baseline test setup" do
+    expect(described_class.new).to be_versioned
+    expect(described_class.inheritance_column).to eq("species")
+  end
+
+  describe "#descends_from_active_record?" do
+    it "returns true, meaning that Animal is not an STI subclass" do
+      expect(described_class.descends_from_active_record?).to eq(true)
+    end
+  end
+
+  it "works with non standard STI column contents" do
+    plant = described_class.create
+    plant.destroy
+
+    tomato = Tomato.create
+    tomato.destroy
+
+    reified = plant.versions.last.reify
+    expect(reified.class).to eq(described_class)
+
+    reified = tomato.versions.last.reify
+    expect(reified.class).to eq(Tomato)
+  end
+end
diff --git a/spec/models/post_spec.rb b/spec/models/post_spec.rb
index 08bb5e2..59bf49f 100644
--- a/spec/models/post_spec.rb
+++ b/spec/models/post_spec.rb
@@ -5,29 +5,29 @@ require "spec_helper"
 # The `Post` model uses a custom version class, `PostVersion`
 RSpec.describe Post, type: :model, versioning: true do
   it "inserts records into the correct table, post_versions" do
-    post = Post.create
+    post = described_class.create
     expect(PostVersion.count).to(eq(1))
     post.update(content: "Some new content")
     expect(PostVersion.count).to(eq(2))
     expect(PaperTrail::Version.count).to(eq(0))
   end
 
-  context "on the first version" do
+  context "with the first version" do
     it "have the correct index" do
-      post = Post.create
+      post = described_class.create
       version = post.versions.first
       expect(version.index).to(eq(0))
     end
   end
 
   it "have versions of the custom class" do
-    post = Post.create
+    post = described_class.create
     expect(post.versions.first.class.name).to(eq("PostVersion"))
   end
 
   describe "#changeset" do
     it "returns nil because the object_changes column doesn't exist" do
-      post = Post.create
+      post = described_class.create
       post.update(content: "Some new content")
       expect(post.versions.last.changeset).to(be_nil)
     end
diff --git a/spec/models/post_with_status_spec.rb b/spec/models/post_with_status_spec.rb
index 3916b73..cd11f05 100644
--- a/spec/models/post_with_status_spec.rb
+++ b/spec/models/post_with_status_spec.rb
@@ -22,7 +22,7 @@ RSpec.describe PostWithStatus, type: :model do
       assert_equal %w[draft published], version.changeset["status"]
     end
 
-    context "storing enum object_changes" do
+    context "when storing enum object_changes" do
       it "saves the enum value properly in versions object_changes" do
         post.published!
         post.archived!
diff --git a/spec/models/skipper_spec.rb b/spec/models/skipper_spec.rb
index 33242fa..2e4189e 100644
--- a/spec/models/skipper_spec.rb
+++ b/spec/models/skipper_spec.rb
@@ -6,12 +6,12 @@ RSpec.describe Skipper, type: :model, versioning: true do
   it { is_expected.to be_versioned }
 
   describe "#update!", versioning: true do
-    context "updating a skipped attribute" do
+    context "when updating a skipped attribute" do
       let(:t1) { Time.zone.local(2015, 7, 15, 20, 34, 0) }
       let(:t2) { Time.zone.local(2015, 7, 15, 20, 34, 30) }
 
       it "does not create a version" do
-        skipper = Skipper.create!(another_timestamp: t1)
+        skipper = described_class.create!(another_timestamp: t1)
         expect {
           skipper.update!(another_timestamp: t2)
         }.not_to(change { skipper.versions.length })
@@ -19,13 +19,55 @@ RSpec.describe Skipper, type: :model, versioning: true do
     end
   end
 
+  describe "#touch" do
+    let(:t1) { Time.zone.local(2015, 7, 15, 20, 34, 0) }
+    let(:t2) { Time.zone.local(2015, 7, 15, 20, 34, 30) }
+
+    if ActiveRecord.gem_version >= Gem::Version.new("6")
+      it "does not create a version for skipped attributes" do
+        skipper = described_class.create!(another_timestamp: t1)
+        expect {
+          skipper.touch(:another_timestamp, time: t2)
+        }.not_to(change { skipper.versions.length })
+      end
+
+      it "does not create a version for ignored attributes" do
+        skipper = described_class.create!(created_at: t1)
+        expect {
+          skipper.touch(:created_at, time: t2)
+        }.not_to(change { skipper.versions.length })
+      end
+    else
+      it "creates a version even for skipped attributes" do
+        skipper = described_class.create!(another_timestamp: t1)
+        expect {
+          skipper.touch(:another_timestamp, time: t2)
+        }.to(change { skipper.versions.length })
+      end
+
+      it "creates a version even for ignored attributes" do
+        skipper = described_class.create!(created_at: t1)
+        expect {
+          skipper.touch(:created_at, time: t2)
+        }.to(change { skipper.versions.length })
+      end
+    end
+
+    it "creates a version for non-skipped timestamps" do
+      skipper = described_class.create!
+      expect {
+        skipper.touch
+      }.to(change { skipper.versions.length })
+    end
+  end
+
   describe "#reify" do
     let(:t1) { Time.zone.local(2015, 7, 15, 20, 34, 0) }
     let(:t2) { Time.zone.local(2015, 7, 15, 20, 34, 30) }
 
     context "without preserve (default)" do
       it "has no timestamp" do
-        skipper = Skipper.create!(another_timestamp: t1)
+        skipper = described_class.create!(another_timestamp: t1)
         skipper.update!(another_timestamp: t2, name: "Foobar")
         skipper = skipper.versions.last.reify
         expect(skipper.another_timestamp).to be(nil)
@@ -34,7 +76,7 @@ RSpec.describe Skipper, type: :model, versioning: true do
 
     context "with preserve" do
       it "preserves its timestamp" do
-        skipper = Skipper.create!(another_timestamp: t1)
+        skipper = described_class.create!(another_timestamp: t1)
         skipper.update!(another_timestamp: t2, name: "Foobar")
         skipper = skipper.versions.last.reify(unversioned_attributes: :preserve)
         expect(skipper.another_timestamp).to eq(t2)
diff --git a/spec/models/song_spec.rb b/spec/models/song_spec.rb
index 91f44e9..370db79 100644
--- a/spec/models/song_spec.rb
+++ b/spec/models/song_spec.rb
@@ -14,4 +14,25 @@ require "spec_helper"
       expect(result.event).to eq("create")
     end
   end
+
+  context "when the default accessor, length=, is overwritten" do
+    it "returns overwritten value on reified instance" do
+      song = Song.create(length: 4)
+      song.update(length: 5)
+      expect(song.length).to(eq(5))
+      expect(song.versions.last.reify.length).to(eq(4))
+    end
+  end
+
+  context "when song name is a virtual attribute (no such db column)" do
+    it "returns overwritten virtual attribute on the reified instance" do
+      song = Song.create(length: 4)
+      song.update(length: 5)
+      song.name = "Good Vibrations"
+      song.save
+      song.name = "Yellow Submarine"
+      expect(song.name).to(eq("Yellow Submarine"))
+      expect(song.versions.last.reify.name).to(eq("Good Vibrations"))
+    end
+  end
 end
diff --git a/spec/models/thing_spec.rb b/spec/models/thing_spec.rb
index ac7b852..3030abd 100644
--- a/spec/models/thing_spec.rb
+++ b/spec/models/thing_spec.rb
@@ -4,10 +4,10 @@ require "spec_helper"
 
 RSpec.describe Thing, type: :model do
   describe "#versions", versioning: true do
-    let(:thing) { Thing.create! }
+    let(:thing) { described_class.create! }
 
     it "applies the scope option" do
-      expect(Thing.reflect_on_association(:versions).scope).to be_a Proc
+      expect(described_class.reflect_on_association(:versions).scope).to be_a Proc
       expect(thing.versions.to_sql).to end_with "ORDER BY id desc"
     end
 
diff --git a/spec/models/translation_spec.rb b/spec/models/translation_spec.rb
index d00947b..23bb145 100644
--- a/spec/models/translation_spec.rb
+++ b/spec/models/translation_spec.rb
@@ -3,13 +3,13 @@
 require "spec_helper"
 
 RSpec.describe Translation, type: :model, versioning: true do
-  context "for non-US translations" do
+  context "with non-US translations" do
     it "not change the number of versions" do
       described_class.create!(headline: "Headline")
       expect(PaperTrail::Version.count).to(eq(0))
     end
 
-    context "after update" do
+    context "when after update" do
       it "not change the number of versions" do
         translation = described_class.create!(headline: "Headline")
         translation.update(content: "Content")
@@ -17,21 +17,29 @@ RSpec.describe Translation, type: :model, versioning: true do
       end
     end
 
-    context "after destroy" do
+    context "when after destroy" do
       it "not change the number of versions" do
         translation = described_class.create!(headline: "Headline")
         translation.destroy
         expect(PaperTrail::Version.count).to(eq(0))
       end
     end
+
+    context "when after touch" do
+      it "not change the number of versions" do
+        translation = described_class.create!(headline: "Headline")
+        translation.touch
+        expect(PaperTrail::Version.count).to(eq(0))
+      end
+    end
   end
 
-  context "for US translations" do
-    context "that are drafts" do
+  context "with US translations" do
+    context "with drafts" do
       it "creation does not change the number of versions" do
         translation = described_class.new(headline: "Headline")
         translation.language_code = "US"
-        translation.type = "DRAFT"
+        translation.draft_status = "DRAFT"
         translation.save!
         expect(PaperTrail::Version.count).to(eq(0))
       end
@@ -39,27 +47,43 @@ RSpec.describe Translation, type: :model, versioning: true do
       it "update does not change the number of versions" do
         translation = described_class.new(headline: "Headline")
         translation.language_code = "US"
-        translation.type = "DRAFT"
+        translation.draft_status = "DRAFT"
         translation.save!
         translation.update(content: "Content")
         expect(PaperTrail::Version.count).to(eq(0))
       end
+
+      it "touch does not change the number of versions" do
+        translation = described_class.new(headline: "Headline")
+        translation.language_code = "US"
+        translation.draft_status = "DRAFT"
+        translation.save!
+        translation.touch
+        expect(PaperTrail::Version.count).to(eq(0))
+      end
     end
 
-    context "that are not drafts" do
+    context "with non-drafts" do
       it "create changes the number of versions" do
         described_class.create!(headline: "Headline", language_code: "US")
         expect(PaperTrail::Version.count).to(eq(1))
       end
 
-      it "update does not change the number of versions" do
+      it "update changes the number of versions" do
         translation = described_class.create!(headline: "Headline", language_code: "US")
         translation.update(content: "Content")
         expect(PaperTrail::Version.count).to(eq(2))
         expect(translation.versions.size).to(eq(2))
       end
 
-      it "destroy does not change the number of versions" do
+      it "touch changes the number of versions" do
+        translation = described_class.create!(headline: "Headline", language_code: "US")
+        translation.touch
+        expect(PaperTrail::Version.count).to(eq(2))
+        expect(translation.versions.size).to(eq(2))
+      end
+
+      it "destroy changes the number of versions" do
         translation = described_class.new(headline: "Headline")
         translation.language_code = "US"
         translation.save!
diff --git a/spec/models/vegetable_spec.rb b/spec/models/vegetable_spec.rb
new file mode 100644
index 0000000..7ab422a
--- /dev/null
+++ b/spec/models/vegetable_spec.rb
@@ -0,0 +1,43 @@
+# frozen_string_literal: true
+
+require "spec_helper"
+require "support/performance_helpers"
+
+if ENV["DB"] == "postgres" && JsonbVersion.table_exists?
+  ::RSpec.describe Vegetable do
+    describe "queries of versions", versioning: true do
+      let!(:vegetable) { described_class.create(name: "Veggie", mass: 1, color: "green") }
+
+      before do
+        vegetable.update(name: "Fidget")
+        vegetable.update(name: "Digit")
+        described_class.create(name: "Cucumber")
+      end
+
+      it "return the vegetable whose name has changed" do
+        result = JsonbVersion.where_attribute_changes(:name).map(&:item)
+        expect(result).to include(vegetable)
+      end
+
+      it "returns the vegetable whose name was Fidget" do
+        result = JsonbVersion.where_object_changes_from({ name: "Fidget" }).map(&:item)
+        expect(result).to include(vegetable)
+      end
+
+      it "returns the vegetable whose name became Digit" do
+        result = JsonbVersion.where_object_changes_to({ name: "Digit" }).map(&:item)
+        expect(result).to include(vegetable)
+      end
+
+      it "returns the vegetable where the object was named Fidget before it changed" do
+        result = JsonbVersion.where_object({ name: "Fidget" }).map(&:item)
+        expect(result).to include(vegetable)
+      end
+
+      it "returns the vegetable that changed to Fidget" do
+        result = JsonbVersion.where_object_changes({ name: "Fidget" }).map(&:item)
+        expect(result).to include(vegetable)
+      end
+    end
+  end
+end
diff --git a/spec/models/version_spec.rb b/spec/models/version_spec.rb
index 761e6e7..4263476 100644
--- a/spec/models/version_spec.rb
+++ b/spec/models/version_spec.rb
@@ -1,14 +1,15 @@
 # frozen_string_literal: true
 
 require "spec_helper"
+require "support/shared_examples/queries"
 
 module PaperTrail
   ::RSpec.describe Version, type: :model do
-    describe "object_changes column", versioning: true do
+    describe "#object_changes", versioning: true do
       let(:widget) { Widget.create!(name: "Dashboard") }
       let(:value) { widget.versions.last.object_changes }
 
-      context "serializer is YAML" do
+      context "when serializer is YAML" do
         specify { expect(PaperTrail.serializer).to be PaperTrail::Serializers::YAML }
 
         it "store out as a plain hash" do
@@ -42,7 +43,7 @@ module PaperTrail
         end
       end
 
-      context "serializer is JSON" do
+      context "when serializer is JSON" do
         before do
           PaperTrail.serializer = PaperTrail::Serializers::JSON
         end
@@ -58,13 +59,13 @@ module PaperTrail
     end
 
     describe "#paper_trail_originator" do
-      context "no previous versions" do
+      context "with no previous versions" do
         it "returns nil" do
-          expect(PaperTrail::Version.new.paper_trail_originator).to be_nil
+          expect(described_class.new.paper_trail_originator).to be_nil
         end
       end
 
-      context "has previous version", versioning: true do
+      context "with previous version", versioning: true do
         it "returns name of whodunnit" do
           name = FFaker::Name.name
           widget = Widget.create!(name: FFaker::Name.name)
@@ -76,19 +77,19 @@ module PaperTrail
     end
 
     describe "#previous" do
-      context "no previous versions" do
+      context "with no previous versions" do
         it "returns nil" do
-          expect(PaperTrail::Version.new.previous).to be_nil
+          expect(described_class.new.previous).to be_nil
         end
       end
 
-      context "has previous version", versioning: true do
+      context "with previous version", versioning: true do
         it "returns a PaperTrail::Version" do
           name = FFaker::Name.name
           widget = Widget.create!(name: FFaker::Name.name)
           widget.versions.first.update!(whodunnit: name)
           widget.update!(name: FFaker::Name.first_name)
-          expect(widget.versions.last.previous).to be_instance_of(PaperTrail::Version)
+          expect(widget.versions.last.previous).to be_instance_of(described_class)
         end
       end
     end
@@ -96,261 +97,39 @@ module PaperTrail
     describe "#terminator" do
       it "is an alias for the `whodunnit` attribute" do
         attributes = { whodunnit: FFaker::Name.first_name }
-        version = PaperTrail::Version.new(attributes)
+        version = described_class.new(attributes)
         expect(version.terminator).to eq(attributes[:whodunnit])
       end
     end
 
     describe "#version_author" do
       it "is an alias for the `terminator` method" do
-        version = PaperTrail::Version.new
+        version = described_class.new
         expect(version.method(:version_author)).to eq(version.method(:terminator))
       end
     end
 
-    context "changing the data type of database columns on the fly" do
-      # TODO: Changing the data type of these database columns in the middle
-      # of the test suite adds a fair amount of complexity. Is there a better
-      # way? We already have a `json_versions` table in our tests, maybe we
-      # could use that and add a `jsonb_versions` table?
-      column_overrides = [false]
-      if ENV["DB"] == "postgres"
-        column_overrides += %w[json jsonb]
-      end
-
-      column_overrides.shuffle.each do |column_datatype_override|
-        context "with a #{column_datatype_override || 'text'} column" do
-          let(:widget) { Widget.new }
-          let(:name) { FFaker::Name.first_name }
-          let(:int) { column_datatype_override ? 1 : rand(2..6) }
-
-          before do
-            if column_datatype_override
-              ActiveRecord::Base.connection.execute("SAVEPOINT pgtest;")
-              %w[object object_changes].each do |column|
-                ActiveRecord::Base.connection.execute(
-                  "ALTER TABLE versions DROP COLUMN #{column};"
-                )
-                ActiveRecord::Base.connection.execute(
-                  "ALTER TABLE versions ADD COLUMN #{column} #{column_datatype_override};"
-                )
-              end
-              PaperTrail::Version.reset_column_information
-            end
-          end
-
-          after do
-            PaperTrail.serializer = PaperTrail::Serializers::YAML
-
-            if column_datatype_override
-              ActiveRecord::Base.connection.execute("ROLLBACK TO SAVEPOINT pgtest;")
-              PaperTrail::Version.reset_column_information
-            end
-          end
-
-          describe "#where_object", versioning: true do
-            it "requires its argument to be a Hash" do
-              widget.update!(name: name, an_integer: int)
-              widget.update!(name: "foobar", an_integer: 100)
-              widget.update!(name: FFaker::Name.last_name, an_integer: 15)
-              expect {
-                PaperTrail::Version.where_object(:foo)
-              }.to raise_error(ArgumentError)
-              expect {
-                PaperTrail::Version.where_object([])
-              }.to raise_error(ArgumentError)
-            end
-
-            context "YAML serializer" do
-              it "locates versions according to their `object` contents" do
-                expect(PaperTrail.serializer).to be PaperTrail::Serializers::YAML
-                widget.update!(name: name, an_integer: int)
-                widget.update!(name: "foobar", an_integer: 100)
-                widget.update!(name: FFaker::Name.last_name, an_integer: 15)
-                expect(
-                  PaperTrail::Version.where_object(an_integer: int)
-                ).to eq([widget.versions[1]])
-                expect(
-                  PaperTrail::Version.where_object(name: name)
-                ).to eq([widget.versions[1]])
-                expect(
-                  PaperTrail::Version.where_object(an_integer: 100)
-                ).to eq([widget.versions[2]])
-              end
-            end
-
-            context "JSON serializer" do
-              it "locates versions according to their `object` contents" do
-                PaperTrail.serializer = PaperTrail::Serializers::JSON
-                expect(PaperTrail.serializer).to be PaperTrail::Serializers::JSON
-                widget.update!(name: name, an_integer: int)
-                widget.update!(name: "foobar", an_integer: 100)
-                widget.update!(name: FFaker::Name.last_name, an_integer: 15)
-                expect(
-                  PaperTrail::Version.where_object(an_integer: int)
-                ).to eq([widget.versions[1]])
-                expect(
-                  PaperTrail::Version.where_object(name: name)
-                ).to eq([widget.versions[1]])
-                expect(
-                  PaperTrail::Version.where_object(an_integer: 100)
-                ).to eq([widget.versions[2]])
-              end
-            end
-          end
-
-          describe "#where_object_changes", versioning: true do
-            it "requires its argument to be a Hash" do
-              expect {
-                PaperTrail::Version.where_object_changes(:foo)
-              }.to raise_error(ArgumentError)
-              expect {
-                PaperTrail::Version.where_object_changes([])
-              }.to raise_error(ArgumentError)
-            end
-
-            context "with object_changes_adapter configured" do
-              after do
-                PaperTrail.config.object_changes_adapter = nil
-              end
-
-              it "calls the adapter's where_object_changes method" do
-                adapter = instance_spy("CustomObjectChangesAdapter")
-                bicycle = Bicycle.create!(name: "abc")
-                allow(adapter).to(
-                  receive(:where_object_changes).with(Version, name: "abc")
-                ).and_return(bicycle.versions[0..1])
-                PaperTrail.config.object_changes_adapter = adapter
-                expect(
-                  bicycle.versions.where_object_changes(name: "abc")
-                ).to match_array(bicycle.versions[0..1])
-                expect(adapter).to have_received(:where_object_changes)
-              end
-
-              it "defaults to the original behavior" do
-                adapter = Class.new.new
-                PaperTrail.config.object_changes_adapter = adapter
-                bicycle = Bicycle.create!(name: "abc")
-                if column_datatype_override
-                  expect(
-                    bicycle.versions.where_object_changes(name: "abc")
-                  ).to match_array(bicycle.versions[0..1])
-                else
-                  expect do
-                    bicycle.versions.where_object_changes(name: "abc")
-                  end.to raise_error(/no longer supports reading YAML/)
-                end
-              end
-            end
-
-            # Only test json and jsonb columns. where_object_changes no longer
-            # supports text columns.
-            if column_datatype_override
-              it "locates versions according to their object_changes contents" do
-                widget.update!(name: name, an_integer: 0)
-                widget.update!(name: "foobar", an_integer: 100)
-                widget.update!(name: FFaker::Name.last_name, an_integer: int)
-                expect(
-                  widget.versions.where_object_changes(name: name)
-                ).to eq(widget.versions[0..1])
-                expect(
-                  widget.versions.where_object_changes(an_integer: 100)
-                ).to eq(widget.versions[1..2])
-                expect(
-                  widget.versions.where_object_changes(an_integer: int)
-                ).to eq([widget.versions.last])
-                expect(
-                  widget.versions.where_object_changes(an_integer: 100, name: "foobar")
-                ).to eq(widget.versions[1..2])
-              end
-            else
-              it "raises error" do
-                expect {
-                  widget.versions.where_object_changes(name: "foo").to_a
-                }.to(raise_error(/no longer supports reading YAML from a text column/))
-              end
-            end
-          end
-
-          describe "#where_object_changes_from", versioning: true do
-            it "requires its argument to be a Hash" do
-              expect {
-                PaperTrail::Version.where_object_changes_from(:foo)
-              }.to raise_error(ArgumentError)
-              expect {
-                PaperTrail::Version.where_object_changes_from([])
-              }.to raise_error(ArgumentError)
-            end
-
-            context "with object_changes_adapter configured" do
-              after do
-                PaperTrail.config.object_changes_adapter = nil
-              end
-
-              it "calls the adapter's where_object_changes_from method" do
-                adapter = instance_spy("CustomObjectChangesAdapter")
-                bicycle = Bicycle.create!(name: "abc")
-                bicycle.update!(name: "xyz")
-
-                allow(adapter).to(
-                  receive(:where_object_changes_from).with(Version, name: "abc")
-                ).and_return([bicycle.versions[1]])
-
-                PaperTrail.config.object_changes_adapter = adapter
-                expect(
-                  bicycle.versions.where_object_changes_from(name: "abc")
-                ).to match_array([bicycle.versions[1]])
-                expect(adapter).to have_received(:where_object_changes_from)
-              end
-
-              it "defaults to the original behavior" do
-                adapter = Class.new.new
-                PaperTrail.config.object_changes_adapter = adapter
-                bicycle = Bicycle.create!(name: "abc")
-                bicycle.update!(name: "xyz")
-
-                if column_datatype_override
-                  expect(
-                    bicycle.versions.where_object_changes_from(name: "abc")
-                  ).to match_array([bicycle.versions[1]])
-                else
-                  expect do
-                    bicycle.versions.where_object_changes_from(name: "abc")
-                  end.to raise_error(/does not support reading YAML/)
-                end
-              end
-            end
+    context "with text columns", versioning: true do
+      include_examples "queries", :text, ::Widget, :an_integer
+    end
 
-            # Only test json and jsonb columns. where_object_changes_from does
-            # not support text columns.
-            if column_datatype_override
-              it "locates versions according to their object_changes contents" do
-                widget.update!(name: name, an_integer: 0)
-                widget.update!(name: "foobar", an_integer: 100)
-                widget.update!(name: FFaker::Name.last_name, an_integer: int)
+    if ENV["DB"] == "postgres"
+      context "with json columns", versioning: true do
+        include_examples(
+          "queries",
+          :json,
+          ::Fruit, # uses JsonVersion
+          :mass
+        )
+      end
 
-                expect(
-                  widget.versions.where_object_changes_from(name: name)
-                ).to eq([widget.versions[1]])
-                expect(
-                  widget.versions.where_object_changes_from(an_integer: 100)
-                ).to eq([widget.versions[2]])
-                expect(
-                  widget.versions.where_object_changes_from(an_integer: int)
-                ).to eq([])
-                expect(
-                  widget.versions.where_object_changes_from(an_integer: 100, name: "foobar")
-                ).to eq([widget.versions[2]])
-              end
-            else
-              it "raises error" do
-                expect {
-                  widget.versions.where_object_changes_from(name: "foo").to_a
-                }.to(raise_error(/does not support reading YAML from a text column/))
-              end
-            end
-          end
-        end
+      context "with jsonb columns", versioning: true do
+        include_examples(
+          "queries",
+          :jsonb,
+          ::Vegetable, # uses JsonbVersion
+          :mass
+        )
       end
     end
   end
diff --git a/spec/models/widget_spec.rb b/spec/models/widget_spec.rb
index 409e703..98557af 100644
--- a/spec/models/widget_spec.rb
+++ b/spec/models/widget_spec.rb
@@ -1,15 +1,749 @@
 # frozen_string_literal: true
 
 require "spec_helper"
+require "support/performance_helpers"
+
+RSpec.describe Widget, type: :model, versioning: true do
+  describe "#changeset" do
+    it "has expected values" do
+      widget = described_class.create(name: "Henry")
+      changeset = widget.versions.last.changeset
+      expect(changeset["name"]).to eq([nil, "Henry"])
+      expect(changeset["id"]).to eq([nil, widget.id])
+      # When comparing timestamps, round off to the nearest second, because
+      # mysql doesn't do fractional seconds.
+      expect(changeset["created_at"][0]).to be_nil
+      expect(changeset["created_at"][1].to_i).to eq(widget.created_at.to_i)
+      expect(changeset["updated_at"][0]).to be_nil
+      expect(changeset["updated_at"][1].to_i).to eq(widget.updated_at.to_i)
+    end
+
+    context "with custom object_changes_adapter" do
+      after do
+        PaperTrail.config.object_changes_adapter = nil
+      end
+
+      it "calls the adapter's load_changeset method" do
+        widget = described_class.create(name: "Henry")
+        adapter = instance_spy("CustomObjectChangesAdapter")
+        PaperTrail.config.object_changes_adapter = adapter
+        allow(adapter).to(
+          receive(:load_changeset).with(widget.versions.last).and_return(a: "b", c: "d")
+        )
+        changeset = widget.versions.last.changeset
+        expect(changeset[:a]).to eq("b")
+        expect(changeset[:c]).to eq("d")
+        expect(adapter).to have_received(:load_changeset)
+      end
+
+      it "defaults to the original behavior" do
+        adapter = Class.new.new
+        PaperTrail.config.object_changes_adapter = adapter
+        widget = described_class.create(name: "Henry")
+        changeset = widget.versions.last.changeset
+        expect(changeset[:name]).to eq([nil, "Henry"])
+      end
+    end
+  end
+
+  describe "#object_changes_deserialized" do
+    context "when the serializer raises a Psych::DisallowedClass error" do
+      it "prints a warning to stderr" do
+        allow(PaperTrail.serializer).to(
+          receive(:load).and_raise(::Psych::Exception, "kaboom")
+        )
+        widget = described_class.create(name: "Henry")
+        ver = widget.versions.last
+        expect { ver.send(:object_changes_deserialized) }.to(
+          output(/kaboom/).to_stderr
+        )
+      end
+    end
+  end
+
+  context "with a new record" do
+    it "not have any previous versions" do
+      expect(described_class.new.versions).to(eq([]))
+    end
+
+    it "be live" do
+      expect(described_class.new.paper_trail.live?).to(eq(true))
+    end
+  end
+
+  context "with a persisted record" do
+    it "have one previous version" do
+      widget = described_class.create(name: "Henry", created_at: (Time.current - 1.day))
+      expect(widget.versions.length).to(eq(1))
+    end
+
+    it "be nil in its previous version" do
+      widget = described_class.create(name: "Henry")
+      expect(widget.versions.first.object).to(be_nil)
+      expect(widget.versions.first.reify).to(be_nil)
+    end
+
+    it "record the correct event" do
+      widget = described_class.create(name: "Henry")
+      expect(widget.versions.first.event).to(match(/create/i))
+    end
+
+    it "be live" do
+      widget = described_class.create(name: "Henry")
+      expect(widget.paper_trail.live?).to(eq(true))
+    end
+
+    it "use the widget `updated_at` as the version's `created_at`" do
+      widget = described_class.create(name: "Henry")
+      expect(widget.versions.first.created_at.to_i).to(eq(widget.updated_at.to_i))
+    end
+
+    context "when updated without any changes" do
+      it "to have two previous versions" do
+        widget = described_class.create(name: "Henry")
+        widget.touch
+        expect(widget.versions.length).to eq(2)
+      end
+    end
+
+    context "when updated with changes" do
+      it "have three previous versions" do
+        widget = described_class.create(name: "Henry")
+        widget.update(name: "Harry")
+        expect(widget.versions.length).to(eq(2))
+      end
+
+      it "be available in its previous version" do
+        widget = described_class.create(name: "Henry")
+        widget.update(name: "Harry")
+        expect(widget.name).to(eq("Harry"))
+        expect(widget.versions.last.object).not_to(be_nil)
+        reified_widget = widget.versions.last.reify
+        expect(reified_widget.name).to(eq("Henry"))
+        expect(widget.name).to(eq("Harry"))
+      end
+
+      it "have the same ID in its previous version" do
+        widget = described_class.create(name: "Henry")
+        widget.update(name: "Harry")
+        expect(widget.versions.last.reify.id).to(eq(widget.id))
+      end
+
+      it "record the correct event" do
+        widget = described_class.create(name: "Henry")
+        widget.update(name: "Harry")
+        expect(widget.versions.last.event).to(match(/update/i))
+      end
+
+      it "have versions that are not live" do
+        widget = described_class.create(name: "Henry")
+        widget.update(name: "Harry")
+        widget.versions.filter_map(&:reify).each do |v|
+          expect(v.paper_trail).not_to be_live
+        end
+      end
+
+      it "have stored changes" do
+        widget = described_class.create(name: "Henry")
+        widget.update(name: "Harry")
+        last_obj_changes = widget.versions.last.object_changes
+        actual = PaperTrail.serializer.load(last_obj_changes).reject do |k, _v|
+          (k.to_sym == :updated_at)
+        end
+        expect(actual).to(eq("name" => %w[Henry Harry]))
+        actual = widget.versions.last.changeset.reject { |k, _v| (k.to_sym == :updated_at) }
+        expect(actual).to(eq("name" => %w[Henry Harry]))
+      end
+
+      it "return changes with indifferent access" do
+        widget = described_class.create(name: "Henry")
+        widget.update(name: "Harry")
+        expect(widget.versions.last.changeset[:name]).to(eq(%w[Henry Harry]))
+        expect(widget.versions.last.changeset["name"]).to(eq(%w[Henry Harry]))
+      end
+    end
+
+    context "when updated, and has one associated object" do
+      it "not copy the has_one association by default when reifying" do
+        widget = described_class.create(name: "Henry")
+        widget.update(name: "Harry")
+        wotsit = widget.create_wotsit name: "John"
+        reified_widget = widget.versions.last.reify
+        expect(reified_widget.wotsit).to eq(wotsit)
+        expect(widget.reload.wotsit).to eq(wotsit)
+      end
+    end
+
+    context "when updated, and has many associated objects" do
+      it "copy the has_many associations when reifying" do
+        widget = described_class.create(name: "Henry")
+        widget.update(name: "Harry")
+        widget.fluxors.create(name: "f-zero")
+        widget.fluxors.create(name: "f-one")
+        reified_widget = widget.versions.last.reify
+        expect(reified_widget.fluxors.length).to(eq(widget.fluxors.length))
+        expect(reified_widget.fluxors).to match_array(widget.fluxors)
+        expect(reified_widget.versions.length).to(eq(widget.versions.length))
+        expect(reified_widget.versions).to match_array(widget.versions)
+      end
+    end
+
+    context "when updated, and has many associated polymorphic objects" do
+      it "copy the has_many associations when reifying" do
+        widget = described_class.create(name: "Henry")
+        widget.update(name: "Harry")
+        widget.whatchamajiggers.create(name: "f-zero")
+        widget.whatchamajiggers.create(name: "f-zero")
+        reified_widget = widget.versions.last.reify
+        expect(reified_widget.whatchamajiggers.length).to eq(widget.whatchamajiggers.length)
+        expect(reified_widget.whatchamajiggers).to match_array(widget.whatchamajiggers)
+        expect(reified_widget.versions.length).to(eq(widget.versions.length))
+        expect(reified_widget.versions).to match_array(widget.versions)
+      end
+    end
+
+    context "when updated, polymorphic objects by themselves" do
+      it "not fail with a nil pointer on the polymorphic association" do
+        widget = described_class.create(name: "Henry")
+        widget.update(name: "Harry")
+        widget = Whatchamajigger.new(name: "f-zero")
+        widget.save!
+      end
+    end
+
+    context "when updated, and then destroyed" do
+      it "record the correct event" do
+        widget = described_class.create(name: "Henry")
+        widget.update(name: "Harry")
+        widget.destroy
+        expect(PaperTrail::Version.last.event).to(match(/destroy/i))
+      end
+
+      it "have three previous versions" do
+        widget = described_class.create(name: "Henry")
+        widget.update(name: "Harry")
+        widget.destroy
+        expect(PaperTrail::Version.with_item_keys("Widget", widget.id).length).to(eq(3))
+      end
+
+      it "returns the expected attributes for the reified widget" do
+        widget = described_class.create(name: "Henry")
+        widget.update(name: "Harry")
+        widget.destroy
+        reified_widget = PaperTrail::Version.last.reify
+        expect(reified_widget.id).to eq(widget.id)
+        expected = widget.attributes
+        actual = reified_widget.attributes
+        expect(expected["id"]).to eq(actual["id"])
+        expect(expected["name"]).to eq(actual["name"])
+        expect(expected["a_text"]).to eq(actual["a_text"])
+        expect(expected["an_integer"]).to eq(actual["an_integer"])
+        expect(expected["a_float"]).to eq(actual["a_float"])
+        expect(expected["a_decimal"]).to eq(actual["a_decimal"])
+        expect(expected["a_datetime"]).to eq(actual["a_datetime"])
+        expect(expected["a_time"]).to eq(actual["a_time"])
+        expect(expected["a_date"]).to eq(actual["a_date"])
+        expect(expected["a_boolean"]).to eq(actual["a_boolean"])
+        expect(expected["type"]).to eq(actual["type"])
+
+        # We are using `to_i` to truncate to the nearest second, but isn't
+        # there still a chance of this failing intermittently if
+        # ___ and ___ occured more than 0.5s apart?
+        expect(expected["created_at"].to_i).to eq(actual["created_at"].to_i)
+        expect(expected["updated_at"].to_i).to eq(actual["updated_at"].to_i)
+      end
+
+      it "be re-creatable from its previous version" do
+        widget = described_class.create(name: "Henry")
+        widget.update(name: "Harry")
+        widget.destroy
+        reified_widget = PaperTrail::Version.last.reify
+        expect(reified_widget.save).to(be_truthy)
+      end
+
+      it "restore its associations on its previous version" do
+        widget = described_class.create(name: "Henry")
+        widget.update(name: "Harry")
+        widget.fluxors.create(name: "flux")
+        widget.destroy
+        reified_widget = PaperTrail::Version.last.reify
+        reified_widget.save
+        expect(reified_widget.fluxors.length).to(eq(1))
+      end
+
+      it "have nil item for last version" do
+        widget = described_class.create(name: "Henry")
+        widget.update(name: "Harry")
+        widget.destroy
+        expect(widget.versions.first.item.object_id).not_to eq(widget.object_id)
+        expect(widget.versions.last.item.object_id).not_to eq(widget.object_id)
+        expect(widget.versions.last.item).to be_nil
+      end
+    end
+  end
+
+  context "with a record's papertrail" do
+    let!(:d0) { Date.new(2009, 5, 29) }
+    let!(:t0) { Time.current }
+    let(:previous_widget) { widget.versions.last.reify }
+    let(:widget) {
+      described_class.create(
+        name: "Warble",
+        a_text: "The quick brown fox",
+        an_integer: 42,
+        a_float: 153.01,
+        a_decimal: 2.71828,
+        a_datetime: t0,
+        a_time: t0,
+        a_date: d0,
+        a_boolean: true
+      )
+    }
+
+    before do
+      widget.update(
+        name: nil,
+        a_text: nil,
+        an_integer: nil,
+        a_float: nil,
+        a_decimal: nil,
+        a_datetime: nil,
+        a_time: nil,
+        a_date: nil,
+        a_boolean: false
+      )
+    end
+
+    it "handle strings" do
+      expect(previous_widget.name).to(eq("Warble"))
+    end
+
+    it "handle text" do
+      expect(previous_widget.a_text).to(eq("The quick brown fox"))
+    end
+
+    it "handle integers" do
+      expect(previous_widget.an_integer).to(eq(42))
+    end
+
+    it "handle floats" do
+      assert_in_delta(153.01, previous_widget.a_float, 0.001)
+    end
+
+    it "handle decimals" do
+      assert_in_delta(2.7183, previous_widget.a_decimal, 0.0001)
+    end
+
+    it "handle datetimes" do
+      expect(previous_widget.a_datetime.to_time.utc.to_i).to(eq(t0.to_time.utc.to_i))
+    end
+
+    it "handle times (time only, no date)" do
+      format = ->(t) { t.utc.strftime "%H:%M:%S" }
+      expect(format[previous_widget.a_time]).to eq(format[t0])
+    end
+
+    it "handle dates" do
+      expect(previous_widget.a_date).to(eq(d0))
+    end
+
+    it "handle booleans" do
+      expect(previous_widget.a_boolean).to(be_truthy)
+    end
+
+    context "when a column has been removed from the record's schema" do
+      let(:last_version) { widget.versions.last }
+
+      it "reify previous version" do
+        assert_kind_of(described_class, last_version.reify)
+      end
+
+      it "restore all forward-compatible attributes" do
+        reified = last_version.reify
+        expect(reified.name).to(eq("Warble"))
+        expect(reified.a_text).to(eq("The quick brown fox"))
+        expect(reified.an_integer).to(eq(42))
+        assert_in_delta(153.01, reified.a_float, 0.001)
+        assert_in_delta(2.7183, reified.a_decimal, 0.0001)
+        expect(reified.a_datetime.to_time.utc.to_i).to(eq(t0.to_time.utc.to_i))
+        format = ->(t) { t.utc.strftime "%H:%M:%S" }
+        expect(format[reified.a_time]).to eq(format[t0])
+        expect(reified.a_date).to(eq(d0))
+        expect(reified.a_boolean).to(be_truthy)
+      end
+    end
+  end
+
+  context "with a record" do
+    context "with PaperTrail globally disabled, when updated" do
+      after { PaperTrail.enabled = true }
+
+      it "not add to its trail" do
+        widget = described_class.create(name: "Zaphod")
+        PaperTrail.enabled = false
+        count = widget.versions.length
+        widget.update(name: "Beeblebrox")
+        expect(widget.versions.length).to(eq(count))
+      end
+    end
+
+    context "with its paper trail turned off, when updated" do
+      after do
+        PaperTrail.request.enable_model(described_class)
+      end
+
+      it "not add to its trail" do
+        widget = described_class.create(name: "Zaphod")
+        PaperTrail.request.disable_model(described_class)
+        count = widget.versions.length
+        widget.update(name: "Beeblebrox")
+        expect(widget.versions.length).to(eq(count))
+      end
+
+      it "add to its trail" do
+        widget = described_class.create(name: "Zaphod")
+        PaperTrail.request.disable_model(described_class)
+        count = widget.versions.length
+        widget.update(name: "Beeblebrox")
+        PaperTrail.request.enable_model(described_class)
+        widget.update(name: "Ford")
+        expect(widget.versions.length).to(eq((count + 1)))
+      end
+    end
+  end
 
-RSpec.describe Widget, type: :model do
-  let(:widget) { Widget.create! name: "Bob", an_integer: 1 }
+  context "with somebody making changes" do
+    context "when a record is created" do
+      it "tracks who made the change" do
+        widget = described_class.new(name: "Fidget")
+        PaperTrail.request.whodunnit = "Alice"
+        widget.save
+        version = widget.versions.last
+        expect(version.whodunnit).to(eq("Alice"))
+        expect(version.paper_trail_originator).to(be_nil)
+        expect(version.terminator).to(eq("Alice"))
+        expect(widget.paper_trail.originator).to(eq("Alice"))
+      end
+    end
+
+    context "when created, then updated" do
+      it "tracks who made the change" do
+        widget = described_class.new(name: "Fidget")
+        PaperTrail.request.whodunnit = "Alice"
+        widget.save
+        PaperTrail.request.whodunnit = "Bob"
+        widget.update(name: "Rivet")
+        version = widget.versions.last
+        expect(version.whodunnit).to(eq("Bob"))
+        expect(version.paper_trail_originator).to(eq("Alice"))
+        expect(version.terminator).to(eq("Bob"))
+        expect(widget.paper_trail.originator).to(eq("Bob"))
+      end
+    end
+
+    context "when created, updated, and destroyed" do
+      it "tracks who made the change" do
+        widget = described_class.new(name: "Fidget")
+        PaperTrail.request.whodunnit = "Alice"
+        widget.save
+        PaperTrail.request.whodunnit = "Bob"
+        widget.update(name: "Rivet")
+        PaperTrail.request.whodunnit = "Charlie"
+        widget.destroy
+        version = PaperTrail::Version.last
+        expect(version.whodunnit).to(eq("Charlie"))
+        expect(version.paper_trail_originator).to(eq("Bob"))
+        expect(version.terminator).to(eq("Charlie"))
+        expect(widget.paper_trail.originator).to(eq("Charlie"))
+      end
+    end
+  end
+
+  context "with an item with versions" do
+    context "when the versions were created over time" do
+      let(:widget) { described_class.create(name: "Widget") }
+      let(:t0) { 2.days.ago }
+      let(:t1) { 1.day.ago }
+      let(:t2) { 1.hour.ago }
+
+      before do
+        widget.update(name: "Fidget")
+        widget.update(name: "Digit")
+        widget.versions[0].update(created_at: t0)
+        widget.versions[1].update(created_at: t1)
+        widget.versions[2].update(created_at: t2)
+        widget.update_attribute(:updated_at, t2)
+      end
+
+      it "return nil for version_at before it was created" do
+        expect(widget.paper_trail.version_at((t0 - 1))).to(be_nil)
+      end
+
+      it "return how it looked when created for version_at its creation" do
+        expect(widget.paper_trail.version_at(t0).name).to(eq("Widget"))
+      end
+
+      it "return how it looked before its first update" do
+        expect(widget.paper_trail.version_at((t1 - 1)).name).to(eq("Widget"))
+      end
+
+      it "return how it looked after its first update" do
+        expect(widget.paper_trail.version_at(t1).name).to(eq("Fidget"))
+      end
+
+      it "return how it looked before its second update" do
+        expect(widget.paper_trail.version_at((t2 - 1)).name).to(eq("Fidget"))
+      end
+
+      it "return how it looked after its second update" do
+        expect(widget.paper_trail.version_at(t2).name).to(eq("Digit"))
+      end
+
+      it "return the current object for version_at after latest update" do
+        expect(widget.paper_trail.version_at(1.day.from_now).name).to(eq("Digit"))
+      end
+
+      it "still return a widget when appropriate, when passing timestamp as string" do
+        expect(
+          widget.paper_trail.version_at((t0 + 1.second).to_s).name
+        ).to(eq("Widget"))
+        expect(
+          widget.paper_trail.version_at((t1 + 1.second).to_s).name
+        ).to(eq("Fidget"))
+        expect(
+          widget.paper_trail.version_at((t2 + 1.second).to_s).name
+        ).to(eq("Digit"))
+      end
+    end
+
+    describe ".versions_between" do
+      it "return versions in the time period" do
+        widget = described_class.create(name: "Widget")
+        widget.update(name: "Fidget")
+        widget.update(name: "Digit")
+        widget.versions[0].update(created_at: 30.days.ago)
+        widget.versions[1].update(created_at: 15.days.ago)
+        widget.versions[2].update(created_at: 1.day.ago)
+        widget.update_attribute(:updated_at, 1.day.ago)
+        expect(
+          widget.paper_trail.versions_between(20.days.ago, 10.days.ago).map(&:name)
+        ).to(eq(["Fidget"]))
+        expect(
+          widget.paper_trail.versions_between(45.days.ago, 10.days.ago).map(&:name)
+        ).to(eq(%w[Widget Fidget]))
+        expect(
+          widget.paper_trail.versions_between(16.days.ago, 1.minute.ago).map(&:name)
+        ).to(eq(%w[Fidget Digit Digit]))
+        expect(
+          widget.paper_trail.versions_between(60.days.ago, 45.days.ago).map(&:name)
+        ).to(eq([]))
+      end
+    end
+
+    context "with the first version" do
+      let(:widget) { described_class.create(name: "Widget") }
+      let(:version) { widget.versions.last }
+
+      before do
+        widget = described_class.create(name: "Widget")
+        widget.update(name: "Fidget")
+        widget.update(name: "Digit")
+      end
+
+      it "have a nil previous version" do
+        expect(version.previous).to(be_nil)
+      end
+
+      it "return the next version" do
+        expect(version.next).to(eq(widget.versions[1]))
+      end
+
+      it "return the correct index" do
+        expect(version.index).to(eq(0))
+      end
+    end
+
+    context "with the last version" do
+      let(:widget) { described_class.create(name: "Widget") }
+      let(:version) { widget.versions.last }
+
+      before do
+        widget.update(name: "Fidget")
+        widget.update(name: "Digit")
+      end
+
+      it "return the previous version" do
+        expect(version.previous).to(eq(widget.versions[(widget.versions.length - 2)]))
+      end
+
+      it "have a nil next version" do
+        expect(version.next).to(be_nil)
+      end
+
+      it "return the correct index" do
+        expect(version.index).to(eq((widget.versions.length - 1)))
+      end
+    end
+  end
+
+  context "with a reified item" do
+    it "know which version it came from, and return its previous self" do
+      widget = described_class.create(name: "Bob")
+      %w[Tom Dick Jane].each do |name|
+        widget.update(name: name)
+      end
+      version = widget.versions.last
+      widget = version.reify
+      expect(widget.version).to(eq(version))
+      expect(widget.paper_trail.previous_version).to(eq(widget.versions[-2].reify))
+    end
+  end
+
+  describe "#next_version" do
+    context "with a reified item" do
+      it "returns the object (not a Version) as it became next" do
+        widget = described_class.create(name: "Bob")
+        %w[Tom Dick Jane].each do |name|
+          widget.update(name: name)
+        end
+        second_widget = widget.versions[1].reify
+        last_widget = widget.versions.last.reify
+        expect(second_widget.paper_trail.next_version.name).to(eq(widget.versions[2].reify.name))
+        expect(widget.name).to(eq(last_widget.paper_trail.next_version.name))
+      end
+    end
+
+    context "with a non-reified item" do
+      it "always returns nil because cannot ever have a next version" do
+        widget = described_class.new
+        expect(widget.paper_trail.next_version).to(be_nil)
+        widget.save
+        %w[Tom Dick Jane].each do |name|
+          widget.update(name: name)
+        end
+        expect(widget.paper_trail.next_version).to(be_nil)
+      end
+    end
+  end
+
+  describe "#previous_version" do
+    context "with a reified item" do
+      it "returns the object (not a Version) as it was most recently" do
+        widget = described_class.create(name: "Bob")
+        %w[Tom Dick Jane].each do |name|
+          widget.update(name: name)
+        end
+        second_widget = widget.versions[1].reify
+        last_widget = widget.versions.last.reify
+        expect(second_widget.paper_trail.previous_version).to(be_nil)
+        expect(last_widget.paper_trail.previous_version.name).to(eq(widget.versions[-2].reify.name))
+      end
+    end
+
+    context "with a non-reified item" do
+      it "returns the object (not a Version) as it was most recently" do
+        widget = described_class.new
+        expect(widget.paper_trail.previous_version).to(be_nil)
+        widget.save
+        %w[Tom Dick Jane].each do |name|
+          widget.update(name: name)
+        end
+        expect(widget.paper_trail.previous_version.name).to(eq(widget.versions.last.reify.name))
+      end
+    end
+  end
+
+  context "with an unsaved record" do
+    it "not have a version created on destroy" do
+      widget = described_class.new
+      widget.destroy
+      expect(widget.versions.empty?).to(eq(true))
+    end
+  end
+
+  context "when measuring the memory allocation of" do
+    let(:widget) do
+      described_class.new(
+        name: "Warble",
+        a_text: "The quick brown fox",
+        an_integer: 42,
+        a_float: 153.01,
+        a_decimal: 2.71828,
+        a_boolean: true
+      )
+    end
+
+    before do
+      # Json fields for `object` & `object_changes` attributes is most efficient way
+      # to do the things - this way we will save even more RAM, as well as will skip
+      # the whole YAML serialization
+      allow(PaperTrail::Version).to receive(:object_changes_col_is_json?).and_return(true)
+      allow(PaperTrail::Version).to receive(:object_col_is_json?).and_return(true)
+
+      # Force the loading of all lazy things like class definitions,
+      # in order to get the pure benchmark
+      version_building.call
+    end
+
+    describe "#build_version_on_create" do
+      let(:version_building) do
+        lambda do
+          widget.paper_trail.send(
+            :build_version_on_create,
+            in_after_callback: false
+          )
+        end
+      end
+
+      it "is frugal enough" do
+        # Some time ago there was 95kbs..
+        # At the time of commit the test passes with assertion on 17kbs.
+        # Lets assert 20kbs then, to avoid flaky fails.
+        expect(&version_building).to allocate_less_than(20).kilobytes
+      end
+    end
+
+    describe "#build_version_on_update" do
+      let(:widget) do
+        super().tap do |w|
+          w.save!
+          w.attributes = {
+            name: "Dostoyevsky",
+            a_text: "The slow yellow mouse",
+            an_integer: 84,
+            a_float: 306.02,
+            a_decimal: 5.43656,
+            a_boolean: false
+          }
+        end
+      end
+      let(:version_building) do
+        lambda do
+          widget.paper_trail.send(
+            :build_version_on_update,
+            force: false,
+            in_after_callback: false,
+            is_touch: false
+          )
+        end
+      end
+
+      it "is frugal enough" do
+        # Some time ago there was 144kbs..
+        # At the time of commit the test passes with assertion on 27kbs.
+        # Lets assert 35kbs then, to avoid flaky fails.
+        expect(&version_building).to allocate_less_than(35).kilobytes
+      end
+    end
+  end
 
   describe "`be_versioned` matcher" do
     it { is_expected.to be_versioned }
   end
 
   describe "`have_a_version_with` matcher", versioning: true do
+    let(:widget) { described_class.create! name: "Bob", an_integer: 1 }
+
     before do
       widget.update!(name: "Leonard", an_integer: 1)
       widget.update!(name: "Tom")
@@ -24,20 +758,24 @@ RSpec.describe Widget, type: :model do
   end
 
   describe "versioning option" do
-    context "enabled", versioning: true do
+    context "when enabled", versioning: true do
       it "enables versioning" do
+        widget = described_class.create! name: "Bob", an_integer: 1
         expect(widget.versions.size).to eq(1)
       end
     end
 
-    context "disabled (default)" do
+    context "when disabled", versioning: false do
       it "does not enable versioning" do
+        widget = described_class.create! name: "Bob", an_integer: 1
         expect(widget.versions.size).to eq(0)
       end
     end
   end
 
   describe "Callbacks", versioning: true do
+    let(:widget) { described_class.create! name: "Bob", an_integer: 1 }
+
     describe "before_save" do
       it "resets value for timestamp attrs for update so that value gets updated properly" do
         widget.update!(name: "Foobar")
@@ -47,7 +785,7 @@ RSpec.describe Widget, type: :model do
     end
 
     describe "after_create" do
-      let(:widget) { Widget.create!(name: "Foobar", created_at: Time.current - 1.week) }
+      let(:widget) { described_class.create!(name: "Foobar", created_at: Time.current - 1.week) }
 
       it "corresponding version uses the widget's `updated_at`" do
         expect(widget.versions.last.created_at.to_i).to eq(widget.updated_at.to_i)
@@ -111,6 +849,8 @@ RSpec.describe Widget, type: :model do
   end
 
   describe "Association", versioning: true do
+    let(:widget) { described_class.create! name: "Bob", an_integer: 1 }
+
     describe "sort order" do
       it "sorts by the timestamp order from the `VersionConcern`" do
         expect(widget.versions.to_sql).to eq(
@@ -120,28 +860,20 @@ RSpec.describe Widget, type: :model do
     end
   end
 
-  if defined?(ActiveRecord::IdentityMap) && ActiveRecord::IdentityMap.respond_to?(:without)
-    describe "IdentityMap", versioning: true do
-      it "does not clobber the IdentityMap when reifying" do
-        widget.update name: "Henry", created_at: Time.current - 1.day
-        widget.update name: "Harry"
-        allow(ActiveRecord::IdentityMap).to receive(:without)
-        widget.versions.last.reify
-        expect(ActiveRecord::IdentityMap).to have_receive(:without).once
-      end
-    end
-  end
-
   describe "#create", versioning: true do
+    let(:widget) { described_class.create! name: "Bob", an_integer: 1 }
+
     it "creates a version record" do
-      wordget = Widget.create
+      wordget = described_class.create
       assert_equal 1, wordget.versions.length
     end
   end
 
   describe "#destroy", versioning: true do
+    let(:widget) { described_class.create! name: "Bob", an_integer: 1 }
+
     it "creates a version record" do
-      widget = Widget.create
+      widget = described_class.create
       assert_equal 1, widget.versions.length
       widget.destroy
       versions_for_widget = PaperTrail::Version.with_item_keys("Widget", widget.id)
@@ -154,7 +886,7 @@ RSpec.describe Widget, type: :model do
         # the `widget.versions` association, instead of `with_item_keys`.
         PaperTrail::Version.with_item_keys("Widget", widget.id)
       }
-      widget = Widget.create
+      widget = described_class.create
       assert_equal 1, widget.versions.length
       widget.destroy
       assert_equal 2, versions.call(widget).length
@@ -168,6 +900,8 @@ RSpec.describe Widget, type: :model do
   end
 
   describe "#paper_trail.originator", versioning: true do
+    let(:widget) { described_class.create! name: "Bob", an_integer: 1 }
+
     describe "return value" do
       let(:orig_name) { FFaker::Name.name }
       let(:new_name) { FFaker::Name.name }
@@ -206,7 +940,9 @@ RSpec.describe Widget, type: :model do
   end
 
   describe "#version_at", versioning: true do
-    context "Timestamp argument is AFTER object has been destroyed" do
+    let(:widget) { described_class.create! name: "Bob", an_integer: 1 }
+
+    context "when Timestamp argument is AFTER object has been destroyed" do
       it "returns nil" do
         widget.update_attribute(:name, "foobar")
         widget.destroy
@@ -216,37 +952,46 @@ RSpec.describe Widget, type: :model do
   end
 
   describe "touch", versioning: true do
+    let(:widget) { described_class.create! name: "Bob", an_integer: 1 }
+
     it "creates a version" do
       expect { widget.touch }.to change {
         widget.versions.count
       }.by(+1)
     end
 
-    context "request is disabled" do
+    context "when request is disabled" do
       it "does not create a version" do
-        count = widget.versions.count
         PaperTrail.request(enabled: false) do
-          widget.touch
+          expect { widget.touch }.not_to(change { widget.versions.count })
         end
-        expect(count).to eq(count)
       end
     end
   end
 
   describe ".paper_trail.update_columns", versioning: true do
     it "creates a version record" do
-      widget = Widget.create
+      widget = described_class.create
       expect(widget.versions.count).to eq(1)
       widget.paper_trail.update_columns(name: "Bugle")
       expect(widget.versions.count).to eq(2)
       expect(widget.versions.last.event).to(eq("update"))
       expect(widget.versions.last.changeset[:name]).to eq([nil, "Bugle"])
     end
+
+    it "uses current time for the version created_at" do
+      widget = described_class.create(updated_at: "2015-01-01 15:00")
+      widget.paper_trail.update_columns(name: "Bugle")
+      version = widget.versions.where(event: "update").last
+      expect(version.created_at.to_f).to be_within(5.0).of(Time.now.to_f)
+    end
   end
 
   describe "#update", versioning: true do
+    let(:widget) { described_class.create! name: "Bob", an_integer: 1 }
+
     it "creates a version record" do
-      widget = Widget.create
+      widget = described_class.create
       assert_equal 1, widget.versions.length
       widget.update(name: "Bugle")
       assert_equal 2, widget.versions.length
diff --git a/spec/models/wotsit_spec.rb b/spec/models/wotsit_spec.rb
new file mode 100644
index 0000000..e72fbef
--- /dev/null
+++ b/spec/models/wotsit_spec.rb
@@ -0,0 +1,18 @@
+# frozen_string_literal: true
+
+require "spec_helper"
+
+RSpec.describe Wotsit, versioning: true do
+  it "update! records timestamps" do
+    wotsit = described_class.create!(name: "wotsit")
+    wotsit.update!(name: "changed")
+    reified = wotsit.versions.last.reify
+    expect(reified.created_at).not_to(be_nil)
+    expect(reified.updated_at).not_to(be_nil)
+  end
+
+  it "update! does not raise error" do
+    wotsit = described_class.create!(name: "name1")
+    expect { wotsit.update!(name: "name2") }.not_to(raise_error)
+  end
+end
diff --git a/spec/paper_trail/cleaner_spec.rb b/spec/paper_trail/cleaner_spec.rb
index 78755bf..33d954f 100644
--- a/spec/paper_trail/cleaner_spec.rb
+++ b/spec/paper_trail/cleaner_spec.rb
@@ -23,7 +23,7 @@ module PaperTrail
         animals.each { |animal| expect(animal.versions.size).to(eq(3)) }
       end
 
-      context "no options provided" do
+      context "with no options provided" do
         it "removes extra versions for each item" do
           PaperTrail.clean_versions!
           expect(PaperTrail::Version.count).to(eq(3))
@@ -38,7 +38,7 @@ module PaperTrail
         end
       end
 
-      context "keeping 2" do
+      context "when keeping 2" do
         it "keeps two records, instead of the usual one" do
           PaperTrail.clean_versions!(keeping: 2)
           expect(PaperTrail::Version.all.count).to(eq(6))
@@ -67,7 +67,7 @@ module PaperTrail
       end
 
       context "with the :item_id option" do
-        context "single ID received" do
+        context "when a single ID is received" do
           it "only deletes the versions for the Item with that ID" do
             PaperTrail.clean_versions!(item_id: animal.id)
             expect(animal.versions.size).to(eq(1))
@@ -75,7 +75,7 @@ module PaperTrail
           end
         end
 
-        context "collection of IDs received" do
+        context "when a collection of IDs is received" do
           it "only deletes versions for the Item(s) with those IDs" do
             PaperTrail.clean_versions!(item_id: [animal.id, dog.id])
             expect(animal.versions.size).to(eq(1))
@@ -85,8 +85,8 @@ module PaperTrail
         end
       end
 
-      context "options combinations" do
-        context ":date" do
+      context "with options combinations" do
+        context "with :date" do
           before do
             [animal, dog].each do |animal|
               animal.versions.each do |ver|
@@ -105,7 +105,7 @@ module PaperTrail
             end
           end
 
-          context "and :keeping" do
+          context "with :keeping" do
             it "restrict cleaning properly" do
               date = animal.versions.first.created_at.to_date
               PaperTrail.clean_versions!(date: date, keeping: 2)
@@ -118,7 +118,7 @@ module PaperTrail
             end
           end
 
-          context "and :item_id" do
+          context "with :item_id" do
             it "restrict cleaning properly" do
               date = animal.versions.first.created_at.to_date
               PaperTrail.clean_versions!(date: date, item_id: dog.id)
@@ -129,7 +129,7 @@ module PaperTrail
             end
           end
 
-          context ", :item_id, and :keeping" do
+          context "with :item_id, and :keeping" do
             it "restrict cleaning properly" do
               date = animal.versions.first.created_at.to_date
               PaperTrail.clean_versions!(date: date, item_id: dog.id, keeping: 2)
@@ -141,7 +141,7 @@ module PaperTrail
           end
         end
 
-        context ":keeping and :item_id" do
+        context "with :keeping and :item_id" do
           it "restrict cleaning properly" do
             PaperTrail.clean_versions!(keeping: 2, item_id: animal.id)
             expect(animal.versions.size).to(eq(2))
diff --git a/spec/paper_trail/compatibility_spec.rb b/spec/paper_trail/compatibility_spec.rb
index ecd2105..79c6597 100644
--- a/spec/paper_trail/compatibility_spec.rb
+++ b/spec/paper_trail/compatibility_spec.rb
@@ -14,7 +14,7 @@ module PaperTrail
 
       context "when incompatible" do
         it "writes a warning to stderr" do
-          ar_version = ::Gem::Version.new("6.2.0")
+          ar_version = ::Gem::Version.new("7.1.0")
           expect {
             described_class.check_activerecord(ar_version)
           }.to output(/not compatible/).to_stderr
diff --git a/spec/paper_trail/config_spec.rb b/spec/paper_trail/config_spec.rb
index f91d1ba..c2e2eaf 100644
--- a/spec/paper_trail/config_spec.rb
+++ b/spec/paper_trail/config_spec.rb
@@ -52,17 +52,6 @@ module PaperTrail
         limited_bike.save
         assert_equal 2, limited_bike.versions.length
       end
-
-      context "when item_subtype column is absent" do
-        it "uses global version_limit" do
-          PaperTrail.config.version_limit = 6
-          names = PaperTrail::Version.column_names - ["item_subtype"]
-          allow(PaperTrail::Version).to receive(:column_names).and_return(names)
-          bike = LimitedBicycle.create!(name: "My Bike") # has_paper_trail limit: 3
-          10.times { bike.update(name: SecureRandom.hex(8)) }
-          assert_equal 7, bike.versions.length
-        end
-      end
     end
   end
 end
diff --git a/spec/paper_trail/events/base_spec.rb b/spec/paper_trail/events/base_spec.rb
index 90b2e26..fd504ce 100644
--- a/spec/paper_trail/events/base_spec.rb
+++ b/spec/paper_trail/events/base_spec.rb
@@ -6,36 +6,36 @@ module PaperTrail
   module Events
     ::RSpec.describe Base do
       describe "#changed_notably?", versioning: true do
-        context "new record" do
+        context "with a new record" do
           it "returns true" do
             g = Gadget.new(created_at: Time.current)
-            event = PaperTrail::Events::Base.new(g, false)
+            event = described_class.new(g, false)
             expect(event.changed_notably?).to eq(true)
           end
         end
 
-        context "persisted record without update timestamps" do
+        context "with a persisted record without update timestamps" do
           it "only acknowledges non-ignored attrs" do
             gadget = Gadget.create!(created_at: Time.current)
             gadget.name = "Wrench"
-            event = PaperTrail::Events::Base.new(gadget, false)
+            event = described_class.new(gadget, false)
             expect(event.changed_notably?).to eq(true)
           end
 
           it "does not acknowledge ignored attr (brand)" do
             gadget = Gadget.create!(created_at: Time.current)
             gadget.brand = "Acme"
-            event = PaperTrail::Events::Base.new(gadget, false)
+            event = described_class.new(gadget, false)
             expect(event.changed_notably?).to eq(false)
           end
         end
 
-        context "persisted record with update timestamps" do
+        context "with a persisted record with update timestamps" do
           it "only acknowledges non-ignored attrs" do
             gadget = Gadget.create!(created_at: Time.current)
             gadget.name = "Wrench"
             gadget.updated_at = Time.current
-            event = PaperTrail::Events::Base.new(gadget, false)
+            event = described_class.new(gadget, false)
             expect(event.changed_notably?).to eq(true)
           end
 
@@ -43,7 +43,7 @@ module PaperTrail
             gadget = Gadget.create!(created_at: Time.current)
             gadget.brand = "Acme"
             gadget.updated_at = Time.current
-            event = PaperTrail::Events::Base.new(gadget, false)
+            event = described_class.new(gadget, false)
             expect(event.changed_notably?).to eq(false)
           end
         end
@@ -53,7 +53,7 @@ module PaperTrail
         it "returns a hash lacking the skipped attribute" do
           # Skipper has_paper_trail(..., skip: [:another_timestamp])
           skipper = Skipper.create!(another_timestamp: Time.current)
-          event = PaperTrail::Events::Base.new(skipper, false)
+          event = described_class.new(skipper, false)
           attributes = event.send(:nonskipped_attributes_before_change, false)
           expect(attributes).not_to have_key("another_timestamp")
         end
diff --git a/spec/paper_trail/events/destroy_spec.rb b/spec/paper_trail/events/destroy_spec.rb
index bfa91f3..f75d3ef 100644
--- a/spec/paper_trail/events/destroy_spec.rb
+++ b/spec/paper_trail/events/destroy_spec.rb
@@ -11,17 +11,21 @@ module PaperTrail
             name: "Carter",
             path_to_stardom: "Mexican radio"
           )
-          data = PaperTrail::Events::Destroy.new(carter, true).data
+          data = described_class.new(carter, true).data
           expect(data[:item_type]).to eq("Family::Family")
           expect(data[:item_subtype]).to eq("Family::CelebrityFamily")
         end
 
-        context "skipper" do
+        context "with skipper" do
           let(:skipper) { Skipper.create!(another_timestamp: Time.current) }
-          let(:data) { PaperTrail::Events::Destroy.new(skipper, false).data }
+          let(:data) { described_class.new(skipper, false).data }
 
           it "includes `object` without skipped attributes" do
-            object = YAML.load(data[:object])
+            object = if ::YAML.respond_to?(:unsafe_load)
+                       YAML.unsafe_load(data[:object])
+                     else
+                       YAML.load(data[:object])
+                     end
             expect(object["id"]).to eq(skipper.id)
             expect(object).to have_key("updated_at")
             expect(object).to have_key("created_at")
@@ -29,7 +33,11 @@ module PaperTrail
           end
 
           it "includes `object_changes` without skipped and ignored attributes" do
-            changes = YAML.load(data[:object_changes])
+            changes = if ::YAML.respond_to?(:unsafe_load)
+                        YAML.unsafe_load(data[:object_changes])
+                      else
+                        YAML.load(data[:object_changes])
+                      end
             expect(changes["id"]).to eq([skipper.id, nil])
             expect(changes["updated_at"][0]).to be_present
             expect(changes["updated_at"][1]).to be_nil
diff --git a/spec/paper_trail/events/update_spec.rb b/spec/paper_trail/events/update_spec.rb
index d16564b..06942cd 100644
--- a/spec/paper_trail/events/update_spec.rb
+++ b/spec/paper_trail/events/update_spec.rb
@@ -6,14 +6,14 @@ module PaperTrail
   module Events
     ::RSpec.describe Update do
       describe "#data", versioning: true do
-        context "is_touch false" do
+        context "when is_touch false" do
           it "object_changes is present" do
             carter = Family::CelebrityFamily.create(
               name: "Carter",
               path_to_stardom: "Mexican radio"
             )
             carter.path_to_stardom = "Johnny"
-            data = PaperTrail::Events::Update.new(carter, false, false, nil).data
+            data = described_class.new(carter, false, false, nil).data
             expect(data[:object_changes]).to eq(
               <<~YAML
                 ---
@@ -25,14 +25,14 @@ module PaperTrail
           end
         end
 
-        context "is_touch true" do
+        context "when is_touch true" do
           it "object_changes is nil" do
             carter = Family::CelebrityFamily.create(
               name: "Carter",
               path_to_stardom: "Mexican radio"
             )
             carter.path_to_stardom = "Johnny"
-            data = PaperTrail::Events::Update.new(carter, false, true, nil).data
+            data = described_class.new(carter, false, true, nil).data
             expect(data[:object_changes]).to be_nil
           end
         end
diff --git a/spec/paper_trail/model_config_spec.rb b/spec/paper_trail/model_config_spec.rb
index ac35914..accb161 100644
--- a/spec/paper_trail/model_config_spec.rb
+++ b/spec/paper_trail/model_config_spec.rb
@@ -8,7 +8,7 @@ module PaperTrail
       describe "versions:" do
         it "name can be passed instead of an options hash", :deprecated do
           allow(::ActiveSupport::Deprecation).to receive(:warn)
-          klass = Class.new(ActiveRecord::Base) do
+          klass = Class.new(ApplicationRecord) do
             has_paper_trail versions: :drafts
           end
           expect(klass.reflect_on_association(:drafts)).to be_a(
@@ -21,7 +21,7 @@ module PaperTrail
         end
 
         it "name can be passed in the options hash" do
-          klass = Class.new(ActiveRecord::Base) do
+          klass = Class.new(ApplicationRecord) do
             has_paper_trail versions: { name: :drafts }
           end
           expect(klass.reflect_on_association(:drafts)).to be_a(
@@ -30,7 +30,7 @@ module PaperTrail
         end
 
         it "class_name can be passed in the options hash" do
-          klass = Class.new(ActiveRecord::Base) do
+          klass = Class.new(ApplicationRecord) do
             has_paper_trail versions: { class_name: "NoObjectVersion" }
           end
           expect(klass.reflect_on_association(:versions).options[:class_name]).to eq(
@@ -39,7 +39,7 @@ module PaperTrail
         end
 
         it "allows any option that has_many supports" do
-          klass = Class.new(ActiveRecord::Base) do
+          klass = Class.new(ApplicationRecord) do
             has_paper_trail versions: { autosave: true, validate: true }
           end
           expect(klass.reflect_on_association(:versions).options[:autosave]).to eq true
@@ -47,7 +47,7 @@ module PaperTrail
         end
 
         it "can even override options that PaperTrail adds to has_many" do
-          klass = Class.new(ActiveRecord::Base) do
+          klass = Class.new(ApplicationRecord) do
             has_paper_trail versions: { as: :foo }
           end
           expect(klass.reflect_on_association(:versions).options[:as]).to eq :foo
@@ -55,7 +55,7 @@ module PaperTrail
 
         it "raises an error on unknown has_many options" do
           expect {
-            Class.new(ActiveRecord::Base) do
+            Class.new(ApplicationRecord) do
               has_paper_trail versions: { read_my_mind: true, validate: true }
             end
           }.to raise_error(
@@ -66,7 +66,7 @@ module PaperTrail
         describe "passing an abstract class to class_name" do
           it "raises an error" do
             expect {
-              Class.new(ActiveRecord::Base) do
+              Class.new(ApplicationRecord) do
                 has_paper_trail versions: { class_name: "AbstractVersion" }
               end
             }.to raise_error(
@@ -79,7 +79,7 @@ module PaperTrail
       describe "class_name:" do
         it "can be used instead of versions: {class_name: ...}", :deprecated do
           allow(::ActiveSupport::Deprecation).to receive(:warn)
-          klass = Class.new(ActiveRecord::Base) do
+          klass = Class.new(ApplicationRecord) do
             has_paper_trail class_name: "NoObjectVersion"
           end
           expect(klass.reflect_on_association(:versions).options[:class_name]).to eq(
diff --git a/spec/paper_trail/model_spec.rb b/spec/paper_trail/model_spec.rb
deleted file mode 100644
index 1fec082..0000000
--- a/spec/paper_trail/model_spec.rb
+++ /dev/null
@@ -1,922 +0,0 @@
-# frozen_string_literal: true
-
-require "spec_helper"
-require "support/performance_helpers"
-
-RSpec.describe(::PaperTrail, versioning: true) do
-  describe "#changeset" do
-    it "has expected values" do
-      widget = Widget.create(name: "Henry")
-      changeset = widget.versions.last.changeset
-      expect(changeset["name"]).to eq([nil, "Henry"])
-      expect(changeset["id"]).to eq([nil, widget.id])
-      # When comparing timestamps, round off to the nearest second, because
-      # mysql doesn't do fractional seconds.
-      expect(changeset["created_at"][0]).to be_nil
-      expect(changeset["created_at"][1].to_i).to eq(widget.created_at.to_i)
-      expect(changeset["updated_at"][0]).to be_nil
-      expect(changeset["updated_at"][1].to_i).to eq(widget.updated_at.to_i)
-    end
-
-    context "custom object_changes_adapter" do
-      after do
-        PaperTrail.config.object_changes_adapter = nil
-      end
-
-      it "calls the adapter's load_changeset method" do
-        widget = Widget.create(name: "Henry")
-        adapter = instance_spy("CustomObjectChangesAdapter")
-        PaperTrail.config.object_changes_adapter = adapter
-        allow(adapter).to(
-          receive(:load_changeset).with(widget.versions.last).and_return(a: "b", c: "d")
-        )
-        changeset = widget.versions.last.changeset
-        expect(changeset[:a]).to eq("b")
-        expect(changeset[:c]).to eq("d")
-        expect(adapter).to have_received(:load_changeset)
-      end
-
-      it "defaults to the original behavior" do
-        adapter = Class.new.new
-        PaperTrail.config.object_changes_adapter = adapter
-        widget = Widget.create(name: "Henry")
-        changeset = widget.versions.last.changeset
-        expect(changeset[:name]).to eq([nil, "Henry"])
-      end
-    end
-  end
-
-  context "a new record" do
-    it "not have any previous versions" do
-      expect(Widget.new.versions).to(eq([]))
-    end
-
-    it "be live" do
-      expect(Widget.new.paper_trail.live?).to(eq(true))
-    end
-  end
-
-  context "a persisted record" do
-    it "have one previous version" do
-      widget = Widget.create(name: "Henry", created_at: (Time.current - 1.day))
-      expect(widget.versions.length).to(eq(1))
-    end
-
-    it "be nil in its previous version" do
-      widget = Widget.create(name: "Henry")
-      expect(widget.versions.first.object).to(be_nil)
-      expect(widget.versions.first.reify).to(be_nil)
-    end
-
-    it "record the correct event" do
-      widget = Widget.create(name: "Henry")
-      expect(widget.versions.first.event).to(match(/create/i))
-    end
-
-    it "be live" do
-      widget = Widget.create(name: "Henry")
-      expect(widget.paper_trail.live?).to(eq(true))
-    end
-
-    it "use the widget `updated_at` as the version's `created_at`" do
-      widget = Widget.create(name: "Henry")
-      expect(widget.versions.first.created_at.to_i).to(eq(widget.updated_at.to_i))
-    end
-
-    context "and then updated without any changes" do
-      it "to have two previous versions" do
-        widget = Widget.create(name: "Henry")
-        widget.touch
-        expect(widget.versions.length).to eq(2)
-      end
-    end
-
-    context "and then updated with changes" do
-      it "have three previous versions" do
-        widget = Widget.create(name: "Henry")
-        widget.update(name: "Harry")
-        expect(widget.versions.length).to(eq(2))
-      end
-
-      it "be available in its previous version" do
-        widget = Widget.create(name: "Henry")
-        widget.update(name: "Harry")
-        expect(widget.name).to(eq("Harry"))
-        expect(widget.versions.last.object).not_to(be_nil)
-        reified_widget = widget.versions.last.reify
-        expect(reified_widget.name).to(eq("Henry"))
-        expect(widget.name).to(eq("Harry"))
-      end
-
-      it "have the same ID in its previous version" do
-        widget = Widget.create(name: "Henry")
-        widget.update(name: "Harry")
-        expect(widget.versions.last.reify.id).to(eq(widget.id))
-      end
-
-      it "record the correct event" do
-        widget = Widget.create(name: "Henry")
-        widget.update(name: "Harry")
-        expect(widget.versions.last.event).to(match(/update/i))
-      end
-
-      it "have versions that are not live" do
-        widget = Widget.create(name: "Henry")
-        widget.update(name: "Harry")
-        widget.versions.map(&:reify).compact.each do |v|
-          expect(v.paper_trail).not_to be_live
-        end
-      end
-
-      it "have stored changes" do
-        widget = Widget.create(name: "Henry")
-        widget.update(name: "Harry")
-        last_obj_changes = widget.versions.last.object_changes
-        actual = PaperTrail.serializer.load(last_obj_changes).reject do |k, _v|
-          (k.to_sym == :updated_at)
-        end
-        expect(actual).to(eq("name" => %w[Henry Harry]))
-        actual = widget.versions.last.changeset.reject { |k, _v| (k.to_sym == :updated_at) }
-        expect(actual).to(eq("name" => %w[Henry Harry]))
-      end
-
-      it "return changes with indifferent access" do
-        widget = Widget.create(name: "Henry")
-        widget.update(name: "Harry")
-        expect(widget.versions.last.changeset[:name]).to(eq(%w[Henry Harry]))
-        expect(widget.versions.last.changeset["name"]).to(eq(%w[Henry Harry]))
-      end
-    end
-
-    context "updated, and has one associated object" do
-      it "not copy the has_one association by default when reifying" do
-        widget = Widget.create(name: "Henry")
-        widget.update(name: "Harry")
-        wotsit = widget.create_wotsit name: "John"
-        reified_widget = widget.versions.last.reify
-        expect(reified_widget.wotsit).to eq(wotsit)
-        expect(widget.reload.wotsit).to eq(wotsit)
-      end
-    end
-
-    context "updated, and has many associated objects" do
-      it "copy the has_many associations when reifying" do
-        widget = Widget.create(name: "Henry")
-        widget.update(name: "Harry")
-        widget.fluxors.create(name: "f-zero")
-        widget.fluxors.create(name: "f-one")
-        reified_widget = widget.versions.last.reify
-        expect(reified_widget.fluxors.length).to(eq(widget.fluxors.length))
-        expect(reified_widget.fluxors).to match_array(widget.fluxors)
-        expect(reified_widget.versions.length).to(eq(widget.versions.length))
-        expect(reified_widget.versions).to match_array(widget.versions)
-      end
-    end
-
-    context "updated, and has many associated polymorphic objects" do
-      it "copy the has_many associations when reifying" do
-        widget = Widget.create(name: "Henry")
-        widget.update(name: "Harry")
-        widget.whatchamajiggers.create(name: "f-zero")
-        widget.whatchamajiggers.create(name: "f-zero")
-        reified_widget = widget.versions.last.reify
-        expect(reified_widget.whatchamajiggers.length).to eq(widget.whatchamajiggers.length)
-        expect(reified_widget.whatchamajiggers).to match_array(widget.whatchamajiggers)
-        expect(reified_widget.versions.length).to(eq(widget.versions.length))
-        expect(reified_widget.versions).to match_array(widget.versions)
-      end
-    end
-
-    context "updated, polymorphic objects by themselves" do
-      it "not fail with a nil pointer on the polymorphic association" do
-        widget = Widget.create(name: "Henry")
-        widget.update(name: "Harry")
-        widget = Whatchamajigger.new(name: "f-zero")
-        widget.save!
-      end
-    end
-
-    context "updated, and then destroyed" do
-      it "record the correct event" do
-        widget = Widget.create(name: "Henry")
-        widget.update(name: "Harry")
-        widget.destroy
-        expect(PaperTrail::Version.last.event).to(match(/destroy/i))
-      end
-
-      it "have three previous versions" do
-        widget = Widget.create(name: "Henry")
-        widget.update(name: "Harry")
-        widget.destroy
-        expect(PaperTrail::Version.with_item_keys("Widget", widget.id).length).to(eq(3))
-      end
-
-      it "returns the expected attributes for the reified widget" do
-        widget = Widget.create(name: "Henry")
-        widget.update(name: "Harry")
-        widget.destroy
-        reified_widget = PaperTrail::Version.last.reify
-        expect(reified_widget.id).to eq(widget.id)
-        expected = widget.attributes
-        actual = reified_widget.attributes
-        expect(expected["id"]).to eq(actual["id"])
-        expect(expected["name"]).to eq(actual["name"])
-        expect(expected["a_text"]).to eq(actual["a_text"])
-        expect(expected["an_integer"]).to eq(actual["an_integer"])
-        expect(expected["a_float"]).to eq(actual["a_float"])
-        expect(expected["a_decimal"]).to eq(actual["a_decimal"])
-        expect(expected["a_datetime"]).to eq(actual["a_datetime"])
-        expect(expected["a_time"]).to eq(actual["a_time"])
-        expect(expected["a_date"]).to eq(actual["a_date"])
-        expect(expected["a_boolean"]).to eq(actual["a_boolean"])
-        expect(expected["type"]).to eq(actual["type"])
-
-        # We are using `to_i` to truncate to the nearest second, but isn't
-        # there still a chance of this failing intermittently if
-        # ___ and ___ occured more than 0.5s apart?
-        expect(expected["created_at"].to_i).to eq(actual["created_at"].to_i)
-        expect(expected["updated_at"].to_i).to eq(actual["updated_at"].to_i)
-      end
-
-      it "be re-creatable from its previous version" do
-        widget = Widget.create(name: "Henry")
-        widget.update(name: "Harry")
-        widget.destroy
-        reified_widget = PaperTrail::Version.last.reify
-        expect(reified_widget.save).to(be_truthy)
-      end
-
-      it "restore its associations on its previous version" do
-        widget = Widget.create(name: "Henry")
-        widget.update(name: "Harry")
-        widget.fluxors.create(name: "flux")
-        widget.destroy
-        reified_widget = PaperTrail::Version.last.reify
-        reified_widget.save
-        expect(reified_widget.fluxors.length).to(eq(1))
-      end
-
-      it "have nil item for last version" do
-        widget = Widget.create(name: "Henry")
-        widget.update(name: "Harry")
-        widget.destroy
-        expect(widget.versions.last.item).to be_nil
-      end
-
-      it "has changes" do
-        book = Book.create! title: "A"
-        changes = YAML.load book.versions.last.attributes["object_changes"]
-        expect(changes).to eq("id" => [nil, book.id], "title" => [nil, "A"])
-
-        book.update! title: "B"
-        changes = YAML.load book.versions.last.attributes["object_changes"]
-        expect(changes).to eq("title" => %w[A B])
-
-        book.destroy
-        changes = YAML.load book.versions.last.attributes["object_changes"]
-        expect(changes).to eq("id" => [book.id, nil], "title" => ["B", nil])
-      end
-    end
-  end
-
-  context "a record's papertrail" do
-    let!(:d0) { Date.new(2009, 5, 29) }
-    let!(:t0) { Time.current }
-    let(:previous_widget) { widget.versions.last.reify }
-    let(:widget) {
-      Widget.create(
-        name: "Warble",
-        a_text: "The quick brown fox",
-        an_integer: 42,
-        a_float: 153.01,
-        a_decimal: 2.71828,
-        a_datetime: t0,
-        a_time: t0,
-        a_date: d0,
-        a_boolean: true
-      )
-    }
-
-    before do
-      widget.update(
-        name: nil,
-        a_text: nil,
-        an_integer: nil,
-        a_float: nil,
-        a_decimal: nil,
-        a_datetime: nil,
-        a_time: nil,
-        a_date: nil,
-        a_boolean: false
-      )
-    end
-
-    it "handle strings" do
-      expect(previous_widget.name).to(eq("Warble"))
-    end
-
-    it "handle text" do
-      expect(previous_widget.a_text).to(eq("The quick brown fox"))
-    end
-
-    it "handle integers" do
-      expect(previous_widget.an_integer).to(eq(42))
-    end
-
-    it "handle floats" do
-      assert_in_delta(153.01, previous_widget.a_float, 0.001)
-    end
-
-    it "handle decimals" do
-      assert_in_delta(2.7183, previous_widget.a_decimal, 0.0001)
-    end
-
-    it "handle datetimes" do
-      expect(previous_widget.a_datetime.to_time.utc.to_i).to(eq(t0.to_time.utc.to_i))
-    end
-
-    it "handle times (time only, no date)" do
-      format = ->(t) { t.utc.strftime "%H:%M:%S" }
-      expect(format[previous_widget.a_time]).to eq(format[t0])
-    end
-
-    it "handle dates" do
-      expect(previous_widget.a_date).to(eq(d0))
-    end
-
-    it "handle booleans" do
-      expect(previous_widget.a_boolean).to(be_truthy)
-    end
-
-    context "after a column is removed from the record's schema" do
-      let(:last_version) { widget.versions.last }
-
-      it "reify previous version" do
-        assert_kind_of(Widget, last_version.reify)
-      end
-
-      it "restore all forward-compatible attributes" do
-        reified = last_version.reify
-        expect(reified.name).to(eq("Warble"))
-        expect(reified.a_text).to(eq("The quick brown fox"))
-        expect(reified.an_integer).to(eq(42))
-        assert_in_delta(153.01, reified.a_float, 0.001)
-        assert_in_delta(2.7183, reified.a_decimal, 0.0001)
-        expect(reified.a_datetime.to_time.utc.to_i).to(eq(t0.to_time.utc.to_i))
-        format = ->(t) { t.utc.strftime "%H:%M:%S" }
-        expect(format[reified.a_time]).to eq(format[t0])
-        expect(reified.a_date).to(eq(d0))
-        expect(reified.a_boolean).to(be_truthy)
-      end
-    end
-  end
-
-  context "A record" do
-    context "with PaperTrail globally disabled, when updated" do
-      after { PaperTrail.enabled = true }
-
-      it "not add to its trail" do
-        widget = Widget.create(name: "Zaphod")
-        PaperTrail.enabled = false
-        count = widget.versions.length
-        widget.update(name: "Beeblebrox")
-        expect(widget.versions.length).to(eq(count))
-      end
-    end
-
-    context "with its paper trail turned off, when updated" do
-      after do
-        PaperTrail.request.enable_model(Widget)
-      end
-
-      it "not add to its trail" do
-        widget = Widget.create(name: "Zaphod")
-        PaperTrail.request.disable_model(Widget)
-        count = widget.versions.length
-        widget.update(name: "Beeblebrox")
-        expect(widget.versions.length).to(eq(count))
-      end
-
-      it "add to its trail" do
-        widget = Widget.create(name: "Zaphod")
-        PaperTrail.request.disable_model(Widget)
-        count = widget.versions.length
-        widget.update(name: "Beeblebrox")
-        PaperTrail.request.enable_model(Widget)
-        widget.update(name: "Ford")
-        expect(widget.versions.length).to(eq((count + 1)))
-      end
-    end
-  end
-
-  context "A papertrail with somebody making changes" do
-    context "when a record is created" do
-      it "tracks who made the change" do
-        widget = Widget.new(name: "Fidget")
-        PaperTrail.request.whodunnit = "Alice"
-        widget.save
-        version = widget.versions.last
-        expect(version.whodunnit).to(eq("Alice"))
-        expect(version.paper_trail_originator).to(be_nil)
-        expect(version.terminator).to(eq("Alice"))
-        expect(widget.paper_trail.originator).to(eq("Alice"))
-      end
-    end
-
-    context "when created, then updated" do
-      it "tracks who made the change" do
-        widget = Widget.new(name: "Fidget")
-        PaperTrail.request.whodunnit = "Alice"
-        widget.save
-        PaperTrail.request.whodunnit = "Bob"
-        widget.update(name: "Rivet")
-        version = widget.versions.last
-        expect(version.whodunnit).to(eq("Bob"))
-        expect(version.paper_trail_originator).to(eq("Alice"))
-        expect(version.terminator).to(eq("Bob"))
-        expect(widget.paper_trail.originator).to(eq("Bob"))
-      end
-    end
-
-    context "when created, updated, and destroyed" do
-      it "tracks who made the change" do
-        widget = Widget.new(name: "Fidget")
-        PaperTrail.request.whodunnit = "Alice"
-        widget.save
-        PaperTrail.request.whodunnit = "Bob"
-        widget.update(name: "Rivet")
-        PaperTrail.request.whodunnit = "Charlie"
-        widget.destroy
-        version = PaperTrail::Version.last
-        expect(version.whodunnit).to(eq("Charlie"))
-        expect(version.paper_trail_originator).to(eq("Bob"))
-        expect(version.terminator).to(eq("Charlie"))
-        expect(widget.paper_trail.originator).to(eq("Charlie"))
-      end
-    end
-  end
-
-  it "update! records timestamps" do
-    wotsit = Wotsit.create!(name: "wotsit")
-    wotsit.update!(name: "changed")
-    reified = wotsit.versions.last.reify
-    expect(reified.created_at).not_to(be_nil)
-    expect(reified.updated_at).not_to(be_nil)
-  end
-
-  it "update! does not raise error" do
-    wotsit = Wotsit.create!(name: "name1")
-    expect { wotsit.update!(name: "name2") }.not_to(raise_error)
-  end
-
-  context "A subclass" do
-    let(:foo) { FooWidget.create }
-
-    before do
-      foo.update!(name: "Foo")
-    end
-
-    it "reify with the correct type" do
-      expect(PaperTrail::Version.last.previous).to(eq(foo.versions.first))
-      expect(PaperTrail::Version.last.next).to(be_nil)
-    end
-
-    it "returns the correct originator" do
-      PaperTrail.request.whodunnit = "Ben"
-      foo.update_attribute(:name, "Geoffrey")
-      expect(foo.paper_trail.originator).to(eq(PaperTrail.request.whodunnit))
-    end
-
-    context "when destroyed" do
-      before { foo.destroy }
-
-      it "reify with the correct type" do
-        assert_kind_of(FooWidget, foo.versions.last.reify)
-        expect(PaperTrail::Version.last.previous).to(eq(foo.versions[1]))
-        expect(PaperTrail::Version.last.next).to(be_nil)
-      end
-    end
-  end
-
-  context "An item with versions" do
-    context "which were created over time" do
-      let(:widget) { Widget.create(name: "Widget") }
-      let(:t0) { 2.days.ago }
-      let(:t1) { 1.day.ago }
-      let(:t2) { 1.hour.ago }
-
-      before do
-        widget.update(name: "Fidget")
-        widget.update(name: "Digit")
-        widget.versions[0].update(created_at: t0)
-        widget.versions[1].update(created_at: t1)
-        widget.versions[2].update(created_at: t2)
-        widget.update_attribute(:updated_at, t2)
-      end
-
-      it "return nil for version_at before it was created" do
-        expect(widget.paper_trail.version_at((t0 - 1))).to(be_nil)
-      end
-
-      it "return how it looked when created for version_at its creation" do
-        expect(widget.paper_trail.version_at(t0).name).to(eq("Widget"))
-      end
-
-      it "return how it looked before its first update" do
-        expect(widget.paper_trail.version_at((t1 - 1)).name).to(eq("Widget"))
-      end
-
-      it "return how it looked after its first update" do
-        expect(widget.paper_trail.version_at(t1).name).to(eq("Fidget"))
-      end
-
-      it "return how it looked before its second update" do
-        expect(widget.paper_trail.version_at((t2 - 1)).name).to(eq("Fidget"))
-      end
-
-      it "return how it looked after its second update" do
-        expect(widget.paper_trail.version_at(t2).name).to(eq("Digit"))
-      end
-
-      it "return the current object for version_at after latest update" do
-        expect(widget.paper_trail.version_at(1.day.from_now).name).to(eq("Digit"))
-      end
-
-      it "still return a widget when appropriate, when passing timestamp as string" do
-        expect(
-          widget.paper_trail.version_at((t0 + 1.second).to_s).name
-        ).to(eq("Widget"))
-        expect(
-          widget.paper_trail.version_at((t1 + 1.second).to_s).name
-        ).to(eq("Fidget"))
-        expect(
-          widget.paper_trail.version_at((t2 + 1.second).to_s).name
-        ).to(eq("Digit"))
-      end
-    end
-
-    describe ".versions_between" do
-      it "return versions in the time period" do
-        widget = Widget.create(name: "Widget")
-        widget.update(name: "Fidget")
-        widget.update(name: "Digit")
-        widget.versions[0].update(created_at: 30.days.ago)
-        widget.versions[1].update(created_at: 15.days.ago)
-        widget.versions[2].update(created_at: 1.day.ago)
-        widget.update_attribute(:updated_at, 1.day.ago)
-        expect(
-          widget.paper_trail.versions_between(20.days.ago, 10.days.ago).map(&:name)
-        ).to(eq(["Fidget"]))
-        expect(
-          widget.paper_trail.versions_between(45.days.ago, 10.days.ago).map(&:name)
-        ).to(eq(%w[Widget Fidget]))
-        expect(
-          widget.paper_trail.versions_between(16.days.ago, 1.minute.ago).map(&:name)
-        ).to(eq(%w[Fidget Digit Digit]))
-        expect(
-          widget.paper_trail.versions_between(60.days.ago, 45.days.ago).map(&:name)
-        ).to(eq([]))
-      end
-    end
-
-    context "on the first version" do
-      let(:widget) { Widget.create(name: "Widget") }
-      let(:version) { widget.versions.last }
-
-      before do
-        widget = Widget.create(name: "Widget")
-        widget.update(name: "Fidget")
-        widget.update(name: "Digit")
-      end
-
-      it "have a nil previous version" do
-        expect(version.previous).to(be_nil)
-      end
-
-      it "return the next version" do
-        expect(version.next).to(eq(widget.versions[1]))
-      end
-
-      it "return the correct index" do
-        expect(version.index).to(eq(0))
-      end
-    end
-
-    context "on the last version" do
-      let(:widget) { Widget.create(name: "Widget") }
-      let(:version) { widget.versions.last }
-
-      before do
-        widget.update(name: "Fidget")
-        widget.update(name: "Digit")
-      end
-
-      it "return the previous version" do
-        expect(version.previous).to(eq(widget.versions[(widget.versions.length - 2)]))
-      end
-
-      it "have a nil next version" do
-        expect(version.next).to(be_nil)
-      end
-
-      it "return the correct index" do
-        expect(version.index).to(eq((widget.versions.length - 1)))
-      end
-    end
-  end
-
-  context "An item" do
-    let(:article) { Article.new(title: initial_title) }
-    let(:initial_title) { "Foobar" }
-
-    context "which is created" do
-      before { article.save }
-
-      it "store fixed meta data" do
-        expect(article.versions.last.answer).to(eq(42))
-      end
-
-      it "store dynamic meta data which is independent of the item" do
-        expect(article.versions.last.question).to(eq("31 + 11 = 42"))
-      end
-
-      it "store dynamic meta data which depends on the item" do
-        expect(article.versions.last.article_id).to(eq(article.id))
-      end
-
-      it "store dynamic meta data based on a method of the item" do
-        expect(article.versions.last.action).to(eq(article.action_data_provider_method))
-      end
-
-      it "store dynamic meta data based on an attribute of the item at creation" do
-        expect(article.versions.last.title).to(eq(initial_title))
-      end
-    end
-
-    context "created, then updated" do
-      before do
-        article.save
-        article.update!(content: "Better text.", title: "Rhubarb")
-      end
-
-      it "store fixed meta data" do
-        expect(article.versions.last.answer).to(eq(42))
-      end
-
-      it "store dynamic meta data which is independent of the item" do
-        expect(article.versions.last.question).to(eq("31 + 11 = 42"))
-      end
-
-      it "store dynamic meta data which depends on the item" do
-        expect(article.versions.last.article_id).to(eq(article.id))
-      end
-
-      it "store dynamic meta data based on an attribute of the item prior to the update" do
-        expect(article.versions.last.title).to(eq(initial_title))
-      end
-    end
-
-    context "created, then destroyed" do
-      before do
-        article.save
-        article.destroy
-      end
-
-      it "store fixed metadata" do
-        expect(article.versions.last.answer).to(eq(42))
-      end
-
-      it "store dynamic metadata which is independent of the item" do
-        expect(article.versions.last.question).to(eq("31 + 11 = 42"))
-      end
-
-      it "store dynamic metadata which depends on the item" do
-        expect(article.versions.last.article_id).to(eq(article.id))
-      end
-
-      it "store dynamic metadata based on attribute of item prior to destruction" do
-        expect(article.versions.last.title).to(eq(initial_title))
-      end
-    end
-  end
-
-  context "A reified item" do
-    it "know which version it came from, and return its previous self" do
-      widget = Widget.create(name: "Bob")
-      %w[Tom Dick Jane].each do |name|
-        widget.update(name: name)
-      end
-      version = widget.versions.last
-      widget = version.reify
-      expect(widget.version).to(eq(version))
-      expect(widget.paper_trail.previous_version).to(eq(widget.versions[-2].reify))
-    end
-  end
-
-  describe "#next_version" do
-    context "a reified item" do
-      it "returns the object (not a Version) as it became next" do
-        widget = Widget.create(name: "Bob")
-        %w[Tom Dick Jane].each do |name|
-          widget.update(name: name)
-        end
-        second_widget = widget.versions[1].reify
-        last_widget = widget.versions.last.reify
-        expect(second_widget.paper_trail.next_version.name).to(eq(widget.versions[2].reify.name))
-        expect(widget.name).to(eq(last_widget.paper_trail.next_version.name))
-      end
-    end
-
-    context "a non-reified item" do
-      it "always returns nil because cannot ever have a next version" do
-        widget = Widget.new
-        expect(widget.paper_trail.next_version).to(be_nil)
-        widget.save
-        %w[Tom Dick Jane].each do |name|
-          widget.update(name: name)
-        end
-        expect(widget.paper_trail.next_version).to(be_nil)
-      end
-    end
-  end
-
-  describe "#previous_version" do
-    context "a reified item" do
-      it "returns the object (not a Version) as it was most recently" do
-        widget = Widget.create(name: "Bob")
-        %w[Tom Dick Jane].each do |name|
-          widget.update(name: name)
-        end
-        second_widget = widget.versions[1].reify
-        last_widget = widget.versions.last.reify
-        expect(second_widget.paper_trail.previous_version).to(be_nil)
-        expect(last_widget.paper_trail.previous_version.name).to(eq(widget.versions[-2].reify.name))
-      end
-    end
-
-    context "a non-reified item" do
-      it "returns the object (not a Version) as it was most recently" do
-        widget = Widget.new
-        expect(widget.paper_trail.previous_version).to(be_nil)
-        widget.save
-        %w[Tom Dick Jane].each do |name|
-          widget.update(name: name)
-        end
-        expect(widget.paper_trail.previous_version.name).to(eq(widget.versions.last.reify.name))
-      end
-    end
-  end
-
-  context ":has_many :through" do
-    it "store version on source <<" do
-      book = Book.create(title: "War and Peace")
-      dostoyevsky = Person.create(name: "Dostoyevsky")
-      Person.create(name: "Solzhenitsyn")
-      count = PaperTrail::Version.count
-      (book.authors << dostoyevsky)
-      expect((PaperTrail::Version.count - count)).to(eq(1))
-      expect(book.authorships.first.versions.first).to(eq(PaperTrail::Version.last))
-    end
-
-    it "store version on source create" do
-      book = Book.create(title: "War and Peace")
-      Person.create(name: "Dostoyevsky")
-      Person.create(name: "Solzhenitsyn")
-      count = PaperTrail::Version.count
-      book.authors.create(name: "Tolstoy")
-      expect((PaperTrail::Version.count - count)).to(eq(2))
-      expect(
-        [PaperTrail::Version.order(:id).to_a[-2].item, PaperTrail::Version.last.item]
-      ).to match_array([Person.last, Authorship.last])
-    end
-
-    it "store version on join destroy" do
-      book = Book.create(title: "War and Peace")
-      dostoyevsky = Person.create(name: "Dostoyevsky")
-      Person.create(name: "Solzhenitsyn")
-      (book.authors << dostoyevsky)
-      count = PaperTrail::Version.count
-      book.authorships.reload.last.destroy
-      expect((PaperTrail::Version.count - count)).to(eq(1))
-      expect(PaperTrail::Version.last.reify.book).to(eq(book))
-      expect(PaperTrail::Version.last.reify.author).to(eq(dostoyevsky))
-    end
-
-    it "store version on join clear" do
-      book = Book.create(title: "War and Peace")
-      dostoyevsky = Person.create(name: "Dostoyevsky")
-      Person.create(name: "Solzhenitsyn")
-      book.authors << dostoyevsky
-      count = PaperTrail::Version.count
-      book.authorships.reload.destroy_all
-      expect((PaperTrail::Version.count - count)).to(eq(1))
-      expect(PaperTrail::Version.last.reify.book).to(eq(book))
-      expect(PaperTrail::Version.last.reify.author).to(eq(dostoyevsky))
-    end
-  end
-
-  context "the default accessor, length=, is overwritten" do
-    it "returns overwritten value on reified instance" do
-      song = Song.create(length: 4)
-      song.update(length: 5)
-      expect(song.length).to(eq(5))
-      expect(song.versions.last.reify.length).to(eq(4))
-    end
-  end
-
-  context "song name is a virtual attribute (no such db column)" do
-    it "returns overwritten virtual attribute on the reified instance" do
-      song = Song.create(length: 4)
-      song.update(length: 5)
-      song.name = "Good Vibrations"
-      song.save
-      song.name = "Yellow Submarine"
-      expect(song.name).to(eq("Yellow Submarine"))
-      expect(song.versions.last.reify.name).to(eq("Good Vibrations"))
-    end
-  end
-
-  context "An unsaved record" do
-    it "not have a version created on destroy" do
-      widget = Widget.new
-      widget.destroy
-      expect(widget.versions.empty?).to(eq(true))
-    end
-  end
-
-  context "Memory allocation of" do
-    let(:widget) do
-      Widget.new(
-        name: "Warble",
-        a_text: "The quick brown fox",
-        an_integer: 42,
-        a_float: 153.01,
-        a_decimal: 2.71828,
-        a_boolean: true
-      )
-    end
-
-    before do
-      # Json fields for `object` & `object_changes` attributes is most efficient way
-      # to do the things - this way we will save even more RAM, as well as will skip
-      # the whole YAML serialization
-      allow(PaperTrail::Version).to receive(:object_changes_col_is_json?).and_return(true)
-      allow(PaperTrail::Version).to receive(:object_col_is_json?).and_return(true)
-
-      # Force the loading of all lazy things like class definitions,
-      # in order to get the pure benchmark
-      version_building.call
-    end
-
-    describe "#build_version_on_create" do
-      let(:version_building) do
-        lambda do
-          widget.paper_trail.send(
-            :build_version_on_create,
-            in_after_callback: false
-          )
-        end
-      end
-
-      it "is frugal enough" do
-        # Some time ago there was 95kbs..
-        # At the time of commit the test passes with assertion on 17kbs.
-        # Lets assert 20kbs then, to avoid flaky fails.
-        expect(&version_building).to allocate_less_than(20).kilobytes
-      end
-    end
-
-    describe "#build_version_on_update" do
-      let(:widget) do
-        super().tap do |w|
-          w.save!
-          w.attributes = {
-            name: "Dostoyevsky",
-            a_text: "The slow yellow mouse",
-            an_integer: 84,
-            a_float: 306.02,
-            a_decimal: 5.43656,
-            a_boolean: false
-          }
-        end
-      end
-      let(:version_building) do
-        lambda do
-          widget.paper_trail.send(
-            :build_version_on_update,
-            force: false,
-            in_after_callback: false,
-            is_touch: false
-          )
-        end
-      end
-
-      it "is frugal enough" do
-        # Some time ago there was 144kbs..
-        # At the time of commit the test passes with assertion on 27kbs.
-        # Lets assert 35kbs then, to avoid flaky fails.
-        expect(&version_building).to allocate_less_than(35).kilobytes
-      end
-    end
-  end
-end
diff --git a/spec/paper_trail/request_spec.rb b/spec/paper_trail/request_spec.rb
index d24fd48..5e2dae5 100644
--- a/spec/paper_trail/request_spec.rb
+++ b/spec/paper_trail/request_spec.rb
@@ -103,8 +103,8 @@ module PaperTrail
     end
 
     describe ".with" do
-      context "block given" do
-        context "all allowed options" do
+      context "with a block given" do
+        context "with all allowed options" do
           it "sets options only for the block passed" do
             described_class.whodunnit = "some_whodunnit"
             described_class.enabled_for_model(Widget, true)
@@ -132,7 +132,7 @@ module PaperTrail
           end
         end
 
-        context "some invalid options" do
+        context "with some invalid options" do
           it "raises an invalid option error" do
             subject = proc do
               described_class.with(whodunnit: "blah", invalid_option: "foo") do
@@ -140,13 +140,13 @@ module PaperTrail
               end
             end
 
-            expect { subject.call }.to raise_error(PaperTrail::Request::InvalidOption) do |e|
+            expect { subject.call }.to raise_error(PaperTrail::InvalidOption) do |e|
               expect(e.message).to eq "Invalid option: invalid_option"
             end
           end
         end
 
-        context "all invalid options" do
+        context "with all invalid options" do
           it "raises an invalid option error" do
             subject = proc do
               described_class.with(invalid_option: "foo", other_invalid_option: "blah") do
@@ -154,7 +154,7 @@ module PaperTrail
               end
             end
 
-            expect { subject.call }.to raise_error(PaperTrail::Request::InvalidOption) do |e|
+            expect { subject.call }.to raise_error(PaperTrail::InvalidOption) do |e|
               expect(e.message).to eq "Invalid option: invalid_option"
             end
           end
diff --git a/spec/paper_trail/serializer_spec.rb b/spec/paper_trail/serializer_spec.rb
index 3ee7136..227dc9e 100644
--- a/spec/paper_trail/serializer_spec.rb
+++ b/spec/paper_trail/serializer_spec.rb
@@ -3,8 +3,8 @@
 require "spec_helper"
 require "support/custom_json_serializer"
 
-RSpec.describe(PaperTrail, versioning: true) do
-  context "YAML serializer" do
+RSpec.describe("PaperTrail serializers", versioning: true) do
+  context "with YAML serializer" do
     it "saves the expected YAML in the object column" do
       customer = Customer.create(name: "Some text.")
       original_attributes = PaperTrail::Events::Base.
@@ -19,7 +19,7 @@ RSpec.describe(PaperTrail, versioning: true) do
     end
   end
 
-  context "JSON Serializer" do
+  context "with JSON Serializer" do
     before do
       PaperTrail.configure do |config|
         config.serializer = PaperTrail::Serializers::JSON
@@ -55,7 +55,7 @@ RSpec.describe(PaperTrail, versioning: true) do
     end
   end
 
-  context "Custom Serializer" do
+  context "with Custom Serializer" do
     before do
       PaperTrail.configure { |config| config.serializer = CustomJsonSerializer }
     end
diff --git a/spec/paper_trail/serializers/json_spec.rb b/spec/paper_trail/serializers/json_spec.rb
index 2608369..b8f0fcd 100644
--- a/spec/paper_trail/serializers/json_spec.rb
+++ b/spec/paper_trail/serializers/json_spec.rb
@@ -59,14 +59,6 @@ module PaperTrail
           end
         end
       end
-
-      describe ".where_object_changes_condition" do
-        it "raises error" do
-          expect {
-            described_class.where_object_changes_condition
-          }.to raise_error(/no longer supports/)
-        end
-      end
     end
   end
 end
diff --git a/spec/paper_trail/serializers/yaml_spec.rb b/spec/paper_trail/serializers/yaml_spec.rb
index 92ef85a..31fa453 100644
--- a/spec/paper_trail/serializers/yaml_spec.rb
+++ b/spec/paper_trail/serializers/yaml_spec.rb
@@ -22,6 +22,24 @@ module PaperTrail
           expect(described_class.load(hash.to_yaml)).to eq(hash)
           expect(described_class.load(array.to_yaml)).to eq(array)
         end
+
+        it "calls the expected load method based on Psych version" do
+          # `use_yaml_unsafe_load` was added in 5.2.8.1, 6.0.5.1, 6.1.6.1, and 7.0.3.1
+          if rails_supports_safe_load?
+            allow(::YAML).to receive(:safe_load)
+            described_class.load("string")
+            expect(::YAML).to have_received(:safe_load)
+            # Psych 4+ implements .unsafe_load
+          elsif ::YAML.respond_to?(:unsafe_load)
+            allow(::YAML).to receive(:unsafe_load)
+            described_class.load("string")
+            expect(::YAML).to have_received(:unsafe_load)
+          else # Psych < 4
+            allow(::YAML).to receive(:load)
+            described_class.load("string")
+            expect(::YAML).to have_received(:load)
+          end
+        end
       end
 
       describe ".dump" do
@@ -42,6 +60,16 @@ module PaperTrail
           expect(arel_value(matches.right)).to eq("%\narg1: Val 1\n%")
         end
       end
+
+      private
+
+      def rails_supports_safe_load?
+        # Rails 7.0.3.1 onwards will always support YAML safe loading
+        return true if ::ActiveRecord.gem_version >= Gem::Version.new("7.0.3.1")
+
+        # Older Rails versions may or may not, depending on whether they have been patched.
+        defined?(ActiveRecord::Base.use_yaml_unsafe_load)
+      end
     end
   end
 end
diff --git a/spec/paper_trail/type_serializers/postgres_array_serializer_spec.rb b/spec/paper_trail/type_serializers/postgres_array_serializer_spec.rb
new file mode 100644
index 0000000..4bec2e8
--- /dev/null
+++ b/spec/paper_trail/type_serializers/postgres_array_serializer_spec.rb
@@ -0,0 +1,32 @@
+# frozen_string_literal: true
+
+require "spec_helper"
+
+module PaperTrail
+  module TypeSerializers
+    ::RSpec.describe PostgresArraySerializer do
+      let(:word_array) { [].fill(0, rand(4..8)) { ::FFaker::Lorem.word } }
+      let(:word_array_as_string) { word_array.join("|") }
+
+      let(:the_thing) { described_class.new("foo", "bar") }
+
+      describe ".deserialize" do
+        it "deserializes array to Ruby" do
+          expect(the_thing.deserialize(word_array)).to eq(word_array)
+        end
+
+        it "deserializes string to Ruby array" do
+          allow(the_thing).to receive(:deserialize_with_ar).and_return(word_array)
+          expect(the_thing.deserialize(word_array_as_string)).to eq(word_array)
+          expect(the_thing).to have_received(:deserialize_with_ar)
+        end
+      end
+
+      describe ".dump" do
+        it "serializes Ruby to JSON" do
+          expect(the_thing.serialize(word_array)).to eq(word_array)
+        end
+      end
+    end
+  end
+end
diff --git a/spec/paper_trail/version_limit_spec.rb b/spec/paper_trail/version_limit_spec.rb
index f981b8a..4899ff5 100644
--- a/spec/paper_trail/version_limit_spec.rb
+++ b/spec/paper_trail/version_limit_spec.rb
@@ -21,6 +21,21 @@ module PaperTrail
       # 4 versions = 3 updates + 1 create.
     end
 
+    it "cleans up old versions with limit specified on base class" do
+      PaperTrail.config.version_limit = 10
+
+      Animal.paper_trail_options[:limit] = 5
+      Dog.paper_trail_options = Animal.paper_trail_options.without(:limit)
+
+      dog = Dog.create(name: "Fluffy") # Dog specified has_paper_trail with no limit option
+
+      15.times do |i|
+        dog.update(name: "Name #{i}")
+      end
+      expect(Dog.find(dog.id).versions.count).to eq(6) # Dog uses limit option on base class, Animal
+      # 6 versions = 5 updates + 1 create.
+    end
+
     it "cleans up old versions" do
       PaperTrail.config.version_limit = 10
       widget = Widget.create
diff --git a/spec/paper_trail/version_spec.rb b/spec/paper_trail/version_spec.rb
index e29008b..749a144 100644
--- a/spec/paper_trail/version_spec.rb
+++ b/spec/paper_trail/version_spec.rb
@@ -40,7 +40,7 @@ module PaperTrail
     end
 
     describe ".subsequent" do
-      context "given a timestamp" do
+      context "with a timestamp" do
         it "returns all versions that were created after the timestamp" do
           animal = Animal.create
           2.times do
@@ -54,7 +54,7 @@ module PaperTrail
         end
       end
 
-      context "given a Version" do
+      context "with a Version" do
         it "grab the timestamp from the version and use that as the value" do
           animal = Animal.create
           2.times do
@@ -68,7 +68,7 @@ module PaperTrail
     end
 
     describe ".preceding" do
-      context "given a timestamp" do
+      context "with a timestamp" do
         it "returns all versions that were created before the timestamp" do
           animal = Animal.create
           2.times do
@@ -82,7 +82,7 @@ module PaperTrail
         end
       end
 
-      context "given a Version" do
+      context "with a Version" do
         it "grab the timestamp from the version and use that as the value" do
           animal = Animal.create
           2.times do
diff --git a/spec/paper_trail_spec.rb b/spec/paper_trail_spec.rb
index 68d71f1..5ff6d9a 100644
--- a/spec/paper_trail_spec.rb
+++ b/spec/paper_trail_spec.rb
@@ -50,7 +50,7 @@ RSpec.describe PaperTrail do
     end
   end
 
-  context "default" do
+  context "when default" do
     it "has versioning off by default" do
       expect(described_class).not_to be_enabled
     end
@@ -63,7 +63,7 @@ RSpec.describe PaperTrail do
       expect(described_class).not_to be_enabled
     end
 
-    context "error within `with_versioning` block" do
+    context "when error within `with_versioning` block" do
       it "reverts the value of `PaperTrail.enabled?` to its previous state" do
         expect(described_class).not_to be_enabled
         expect { with_versioning { raise } }.to raise_error(RuntimeError)
@@ -72,7 +72,7 @@ RSpec.describe PaperTrail do
     end
   end
 
-  context "`versioning: true`", versioning: true do
+  context "with `versioning: true`", versioning: true do
     it "has versioning on by default" do
       expect(described_class).to be_enabled
     end
@@ -86,7 +86,7 @@ RSpec.describe PaperTrail do
     end
   end
 
-  context "`with_versioning` block at class level" do
+  context "with `with_versioning` block at class level" do
     it { expect(described_class).not_to be_enabled }
 
     with_versioning do
diff --git a/spec/requests/articles_spec.rb b/spec/requests/articles_spec.rb
index 2d77ca0..9bffdd3 100644
--- a/spec/requests/articles_spec.rb
+++ b/spec/requests/articles_spec.rb
@@ -5,7 +5,7 @@ require "spec_helper"
 RSpec.describe "Articles management", type: :request, order: :defined do
   let(:valid_params) { { article: { title: "Doh", content: FFaker::Lorem.sentence } } }
 
-  context "versioning disabled" do
+  context "with versioning disabled" do
     specify { expect(PaperTrail).not_to be_enabled }
 
     it "does not create a version" do
@@ -19,7 +19,7 @@ RSpec.describe "Articles management", type: :request, order: :defined do
   with_versioning do
     let(:article) { Article.last }
 
-    context "`current_user` method returns a `String`" do
+    context "when `current_user` method returns a `String`" do
       it "sets that value as the `whodunnit`" do
         expect {
           post articles_path, params: valid_params
diff --git a/spec/spec_helper.rb b/spec/spec_helper.rb
index 63b43ec..30484b2 100644
--- a/spec/spec_helper.rb
+++ b/spec/spec_helper.rb
@@ -3,6 +3,12 @@
 ENV["RAILS_ENV"] ||= "test"
 ENV["DB"] ||= "sqlite"
 
+require "simplecov"
+SimpleCov.start do
+  add_filter %w[Appraisals Gemfile Rakefile doc gemfiles spec]
+end
+SimpleCov.minimum_coverage(ENV["DB"] == "postgres" ? 96.8 : 92.4)
+
 require "byebug"
 require_relative "support/pt_arel_helpers"
 
@@ -47,7 +53,7 @@ end
 # in `dummy_app/config/*`. By consolidating it here,
 #
 # - It can better be understood, and documented in one place
-# - It can more closely resememble a conventional app boot. For example, loading
+# - It can more closely resemble a conventional app boot. For example, loading
 # gems (like rspec-rails) _before_ loading the app.
 
 # First, `config/boot.rb` would add gems to $LOAD_PATH.
diff --git a/spec/support/custom_object_changes_adapter.rb b/spec/support/custom_object_changes_adapter.rb
index 419bfe6..19148a5 100644
--- a/spec/support/custom_object_changes_adapter.rb
+++ b/spec/support/custom_object_changes_adapter.rb
@@ -10,6 +10,10 @@ class CustomObjectChangesAdapter
     version.changeset
   end
 
+  def where_attribute_changes(klass, attribute)
+    klass.where(attribute)
+  end
+
   def where_object_changes(klass, attributes)
     klass.where(attributes)
   end
@@ -17,4 +21,8 @@ class CustomObjectChangesAdapter
   def where_object_changes_from(klass, attributes)
     klass.where(attributes)
   end
+
+  def where_object_changes_to(klass, attributes)
+    klass.where(attributes)
+  end
 end
diff --git a/spec/support/paper_trail_spec_migrator.rb b/spec/support/paper_trail_spec_migrator.rb
index edd8f43..f53903c 100644
--- a/spec/support/paper_trail_spec_migrator.rb
+++ b/spec/support/paper_trail_spec_migrator.rb
@@ -18,22 +18,11 @@ class PaperTrailSpecMigrator
     @migrations_path = dummy_app_migrations_dir
   end
 
-  # Looks like the API for programatically running migrations will change
-  # in rails 5.2. This is an undocumented change, AFAICT. Then again,
-  # how many people use the programmatic interface? Most people probably
-  # just use rake. Maybe we're doing it wrong.
-  #
-  # See also discussion in https://github.com/rails/rails/pull/40806, when
-  # MigrationContext#migrate became public.
   def migrate
-    if ::ActiveRecord.gem_version >= ::Gem::Version.new("6.0.0.rc2")
-      ::ActiveRecord::MigrationContext.new(
-        @migrations_path,
-        ::ActiveRecord::Base.connection.schema_migration
-      ).migrate
-    else
-      ::ActiveRecord::MigrationContext.new(@migrations_path).migrate
-    end
+    ::ActiveRecord::MigrationContext.new(
+      @migrations_path,
+      ::ActiveRecord::Base.connection.schema_migration
+    ).migrate
   end
 
   # Generate a migration, run it, and delete it. We use this for testing the
diff --git a/spec/support/performance_helpers.rb b/spec/support/performance_helpers.rb
index 077a984..01c79b0 100644
--- a/spec/support/performance_helpers.rb
+++ b/spec/support/performance_helpers.rb
@@ -33,6 +33,6 @@ RSpec::Matchers.define :allocate_less_than do |expected|
 
   failure_message do
     "expected that example will allocate less than #{expected}#{@scale},"\
-    " but allocated #{@allocated}#{@scale}"
+      " but allocated #{@allocated}#{@scale}"
   end
 end
diff --git a/spec/support/shared_examples/queries.rb b/spec/support/shared_examples/queries.rb
new file mode 100644
index 0000000..6264826
--- /dev/null
+++ b/spec/support/shared_examples/queries.rb
@@ -0,0 +1,388 @@
+# frozen_string_literal: true
+
+RSpec.shared_examples "queries" do |column_type, model, name_of_integer_column|
+  let(:record) { model.new }
+  let(:name) { FFaker::Name.first_name }
+  let(:int) { column_type == :text ? 1 : rand(2..6) }
+
+  after do
+    PaperTrail.serializer = PaperTrail::Serializers::YAML
+  end
+
+  describe "#where_attribute_changes", versioning: true do
+    it "requires its argument to be a string or a symbol" do
+      expect {
+        model.paper_trail.version_class.where_attribute_changes({})
+      }.to raise_error(ArgumentError)
+      expect {
+        model.paper_trail.version_class.where_attribute_changes([])
+      }.to raise_error(ArgumentError)
+    end
+
+    context "with object_changes_adapter configured" do
+      after do
+        PaperTrail.config.object_changes_adapter = nil
+      end
+
+      it "calls the adapter's where_attribute_changes method" do
+        adapter = instance_spy("CustomObjectChangesAdapter")
+        bicycle = model.create!(name: "abc")
+        bicycle.update!(name: "xyz")
+
+        allow(adapter).to(
+          receive(:where_attribute_changes).with(model.paper_trail.version_class, :name)
+        ).and_return([bicycle.versions[0], bicycle.versions[1]])
+
+        PaperTrail.config.object_changes_adapter = adapter
+        expect(
+          bicycle.versions.where_attribute_changes(:name)
+        ).to match_array([bicycle.versions[0], bicycle.versions[1]])
+        expect(adapter).to have_received(:where_attribute_changes)
+      end
+
+      it "defaults to the original behavior" do
+        adapter = Class.new.new
+        PaperTrail.config.object_changes_adapter = adapter
+        bicycle = model.create!(name: "abc")
+        bicycle.update!(name: "xyz")
+
+        if column_type == :text
+          expect {
+            bicycle.versions.where_attribute_changes(:name)
+          }.to raise_error(
+            ::PaperTrail::UnsupportedColumnType,
+            "where_attribute_changes expected json or jsonb column, got text"
+          )
+        else
+          expect(
+            bicycle.versions.where_attribute_changes(:name)
+          ).to match_array([bicycle.versions[0], bicycle.versions[1]])
+        end
+      end
+    end
+
+    if column_type == :text
+      it "raises error" do
+        expect {
+          record.versions.where_attribute_changes(:name).to_a
+        }.to raise_error(
+          ::PaperTrail::UnsupportedColumnType,
+          "where_attribute_changes expected json or jsonb column, got text"
+        )
+      end
+    else
+      it "locates versions according to their object_changes contents" do
+        record.update!(name: "foobar", name_of_integer_column => 100)
+        record.update!(name_of_integer_column => 17)
+
+        expect(
+          record.versions.where_attribute_changes(:name)
+        ).to eq([record.versions[0]])
+        expect(
+          record.versions.where_attribute_changes(name_of_integer_column.to_s)
+        ).to eq([record.versions[0], record.versions[1]])
+        expect(record.class.column_names).to include("color")
+        expect(
+          record.versions.where_attribute_changes(:color)
+        ).to eq([])
+      end
+    end
+  end
+
+  describe "#where_object", versioning: true do
+    it "requires its argument to be a Hash" do
+      record.update!(name: name, name_of_integer_column => int)
+      record.update!(name: "foobar", name_of_integer_column => 100)
+      record.update!(name: FFaker::Name.last_name, name_of_integer_column => 15)
+      expect {
+        model.paper_trail.version_class.where_object(:foo)
+      }.to raise_error(ArgumentError)
+      expect {
+        model.paper_trail.version_class.where_object([])
+      }.to raise_error(ArgumentError)
+    end
+
+    context "with YAML serializer" do
+      it "locates versions according to their `object` contents" do
+        expect(PaperTrail.serializer).to be PaperTrail::Serializers::YAML
+        record.update!(name: name, name_of_integer_column => int)
+        record.update!(name: "foobar", name_of_integer_column => 100)
+        record.update!(name: FFaker::Name.last_name, name_of_integer_column => 15)
+        expect(
+          model.paper_trail.version_class.where_object(name_of_integer_column => int)
+        ).to eq([record.versions[1]])
+        expect(
+          model.paper_trail.version_class.where_object(name: name)
+        ).to eq([record.versions[1]])
+        expect(
+          model.paper_trail.version_class.where_object(name_of_integer_column => 100)
+        ).to eq([record.versions[2]])
+      end
+    end
+
+    context "with JSON serializer" do
+      it "locates versions according to their `object` contents" do
+        PaperTrail.serializer = PaperTrail::Serializers::JSON
+        expect(PaperTrail.serializer).to be PaperTrail::Serializers::JSON
+        record.update!(name: name, name_of_integer_column => int)
+        record.update!(name: "foobar", name_of_integer_column => 100)
+        record.update!(name: FFaker::Name.last_name, name_of_integer_column => 15)
+        expect(
+          model.paper_trail.version_class.where_object(name_of_integer_column => int)
+        ).to eq([record.versions[1]])
+        expect(
+          model.paper_trail.version_class.where_object(name: name)
+        ).to eq([record.versions[1]])
+        expect(
+          model.paper_trail.version_class.where_object(name_of_integer_column => 100)
+        ).to eq([record.versions[2]])
+      end
+    end
+  end
+
+  describe "#where_object_changes", versioning: true do
+    it "requires its argument to be a Hash" do
+      expect {
+        model.paper_trail.version_class.where_object_changes(:foo)
+      }.to raise_error(ArgumentError)
+      expect {
+        model.paper_trail.version_class.where_object_changes([])
+      }.to raise_error(ArgumentError)
+    end
+
+    context "with object_changes_adapter configured" do
+      after do
+        PaperTrail.config.object_changes_adapter = nil
+      end
+
+      it "calls the adapter's where_object_changes method" do
+        adapter = instance_spy("CustomObjectChangesAdapter")
+        bicycle = model.create!(name: "abc")
+        allow(adapter).to(
+          receive(:where_object_changes).with(model.paper_trail.version_class, { name: "abc" })
+        ).and_return(bicycle.versions[0..1])
+        PaperTrail.config.object_changes_adapter = adapter
+        expect(
+          bicycle.versions.where_object_changes(name: "abc")
+        ).to match_array(bicycle.versions[0..1])
+        expect(adapter).to have_received(:where_object_changes)
+      end
+
+      it "defaults to the original behavior" do
+        adapter = Class.new.new
+        PaperTrail.config.object_changes_adapter = adapter
+        bicycle = model.create!(name: "abc")
+        if column_type == :text
+          expect {
+            bicycle.versions.where_object_changes(name: "abc")
+          }.to raise_error(
+            ::PaperTrail::UnsupportedColumnType,
+            "where_object_changes expected json or jsonb column, got text"
+          )
+        else
+          expect(
+            bicycle.versions.where_object_changes(name: "abc")
+          ).to match_array(bicycle.versions[0..1])
+        end
+      end
+    end
+
+    if column_type == :text
+      it "raises error" do
+        expect {
+          record.versions.where_object_changes(name: "foo").to_a
+        }.to raise_error(
+          ::PaperTrail::UnsupportedColumnType,
+          "where_object_changes expected json or jsonb column, got text"
+        )
+      end
+    else
+      it "locates versions according to their object_changes contents" do
+        record.update!(name: name, name_of_integer_column => 0)
+        record.update!(name: "foobar", name_of_integer_column => 100)
+        record.update!(name: FFaker::Name.last_name, name_of_integer_column => int)
+        expect(
+          record.versions.where_object_changes(name: name)
+        ).to eq(record.versions[0..1])
+        expect(
+          record.versions.where_object_changes(name_of_integer_column => 100)
+        ).to eq(record.versions[1..2])
+        expect(
+          record.versions.where_object_changes(name_of_integer_column => int)
+        ).to eq([record.versions.last])
+        expect(
+          record.versions.where_object_changes(name_of_integer_column => 100, name: "foobar")
+        ).to eq(record.versions[1..2])
+      end
+    end
+  end
+
+  describe "#where_object_changes_from", versioning: true do
+    it "requires its argument to be a Hash" do
+      expect {
+        model.paper_trail.version_class.where_object_changes_from(:foo)
+      }.to raise_error(ArgumentError)
+      expect {
+        model.paper_trail.version_class.where_object_changes_from([])
+      }.to raise_error(ArgumentError)
+    end
+
+    context "with object_changes_adapter configured" do
+      after do
+        PaperTrail.config.object_changes_adapter = nil
+      end
+
+      it "calls the adapter's where_object_changes_from method" do
+        adapter = instance_spy("CustomObjectChangesAdapter")
+        bicycle = model.create!(name: "abc")
+        bicycle.update!(name: "xyz")
+
+        allow(adapter).to(
+          receive(:where_object_changes_from).with(model.paper_trail.version_class, { name: "abc" })
+        ).and_return([bicycle.versions[1]])
+
+        PaperTrail.config.object_changes_adapter = adapter
+        expect(
+          bicycle.versions.where_object_changes_from(name: "abc")
+        ).to match_array([bicycle.versions[1]])
+        expect(adapter).to have_received(:where_object_changes_from)
+      end
+
+      it "defaults to the original behavior" do
+        adapter = Class.new.new
+        PaperTrail.config.object_changes_adapter = adapter
+        bicycle = model.create!(name: "abc")
+        bicycle.update!(name: "xyz")
+
+        if column_type == :text
+          expect {
+            bicycle.versions.where_object_changes_from(name: "abc")
+          }.to raise_error(
+            ::PaperTrail::UnsupportedColumnType,
+            "where_object_changes_from expected json or jsonb column, got text"
+          )
+        else
+          expect(
+            bicycle.versions.where_object_changes_from(name: "abc")
+          ).to match_array([bicycle.versions[1]])
+        end
+      end
+    end
+
+    if column_type == :text
+      it "raises error" do
+        expect {
+          record.versions.where_object_changes_from(name: "foo").to_a
+        }.to raise_error(
+          ::PaperTrail::UnsupportedColumnType,
+          "where_object_changes_from expected json or jsonb column, got text"
+        )
+      end
+    else
+      it "locates versions according to their object_changes contents" do
+        record.update!(name: name, name_of_integer_column => 0)
+        record.update!(name: "foobar", name_of_integer_column => 100)
+        record.update!(name: FFaker::Name.last_name, name_of_integer_column => int)
+
+        expect(
+          record.versions.where_object_changes_from(name: name)
+        ).to eq([record.versions[1]])
+        expect(
+          record.versions.where_object_changes_from(name_of_integer_column => 100)
+        ).to eq([record.versions[2]])
+        expect(
+          record.versions.where_object_changes_from(name_of_integer_column => int)
+        ).to eq([])
+        expect(
+          record.versions.where_object_changes_from(name_of_integer_column => 100, name: "foobar")
+        ).to eq([record.versions[2]])
+      end
+    end
+  end
+
+  describe "#where_object_changes_to", versioning: true do
+    it "requires its argument to be a Hash" do
+      expect {
+        model.paper_trail.version_class.where_object_changes_to(:foo)
+      }.to raise_error(ArgumentError)
+      expect {
+        model.paper_trail.version_class.where_object_changes_to([])
+      }.to raise_error(ArgumentError)
+    end
+
+    context "with object_changes_adapter configured" do
+      after do
+        PaperTrail.config.object_changes_adapter = nil
+      end
+
+      it "calls the adapter's where_object_changes_to method" do
+        adapter = instance_spy("CustomObjectChangesAdapter")
+        bicycle = model.create!(name: "abc")
+        bicycle.update!(name: "xyz")
+
+        allow(adapter).to(
+          receive(:where_object_changes_to).with(model.paper_trail.version_class, { name: "xyz" })
+        ).and_return([bicycle.versions[1]])
+
+        PaperTrail.config.object_changes_adapter = adapter
+        expect(
+          bicycle.versions.where_object_changes_to(name: "xyz")
+        ).to match_array([bicycle.versions[1]])
+        expect(adapter).to have_received(:where_object_changes_to)
+      end
+
+      it "defaults to the original behavior" do
+        adapter = Class.new.new
+        PaperTrail.config.object_changes_adapter = adapter
+        bicycle = model.create!(name: "abc")
+        bicycle.update!(name: "xyz")
+
+        if column_type == :text
+          expect {
+            bicycle.versions.where_object_changes_to(name: "xyz")
+          }.to raise_error(
+            ::PaperTrail::UnsupportedColumnType,
+            "where_object_changes_to expected json or jsonb column, got text"
+          )
+        else
+          expect(
+            bicycle.versions.where_object_changes_to(name: "xyz")
+          ).to match_array([bicycle.versions[1]])
+        end
+      end
+    end
+
+    if column_type == :text
+      it "raises error" do
+        expect {
+          record.versions.where_object_changes_to(name: "foo").to_a
+        }.to raise_error(
+          ::PaperTrail::UnsupportedColumnType,
+          "where_object_changes_to expected json or jsonb column, got text"
+        )
+      end
+    else
+      it "locates versions according to their object_changes contents" do
+        record.update!(name: name, name_of_integer_column => 0)
+        record.update!(name: "foobar", name_of_integer_column => 100)
+        record.update!(name: FFaker::Name.last_name, name_of_integer_column => int)
+
+        expect(
+          record.versions.where_object_changes_to(name: name)
+        ).to eq([record.versions[0]])
+        expect(
+          record.versions.where_object_changes_to(name_of_integer_column => 100)
+        ).to eq([record.versions[1]])
+        expect(
+          record.versions.where_object_changes_to(name_of_integer_column => int)
+        ).to eq([record.versions[2]])
+        expect(
+          record.versions.where_object_changes_to(name_of_integer_column => 100, name: "foobar")
+        ).to eq([record.versions[1]])
+        expect(
+          record.versions.where_object_changes_to(name_of_integer_column => -1)
+        ).to eq([])
+      end
+    end
+  end
+end

Debdiff

[The following lists of changes regard files as different if they have different names, permissions or owners.]

Files in second set of .debs but not in first

-rw-r--r--  root/root   /usr/lib/ruby/vendor_ruby/paper_trail/errors.rb
-rw-r--r--  root/root   /usr/lib/ruby/vendor_ruby/paper_trail/queries/versions/where_attribute_changes.rb
-rw-r--r--  root/root   /usr/lib/ruby/vendor_ruby/paper_trail/queries/versions/where_object_changes_to.rb
-rw-r--r--  root/root   /usr/share/rubygems-integration/all/specifications/paper_trail-14.0.0.gemspec

Files in first set of .debs but not in second

-rw-r--r--  root/root   /usr/share/rubygems-integration/all/specifications/paper_trail-12.0.0.gemspec

No differences were encountered in the control files

More details

Full run details