Queries: object_changes: Simplify error instantiation

- Introduces a uniform error class, UnsupportedColumnType
- Simplifies the built-in serializers (paper_trail/serializers)

Since 9.2.0, when `object_changes_adapter` was introduced, if someone must use a
text column, and still wants to use these queries, they must write an
`object_changes_adapter`. AFAIK, no one has ever done this. The only public
adapter I know of, paper_trail-hashdiff, only supports json/b columns.

It's also theoretically possible that, after `where_object_changes` dropped
support for text columns, someone wrote a custom serializer (see
`PaperTrail.serializer=`). AFAIK, no one has done that either. Such a technique
was never documented under [6.b. Custom
Serializer](https://github.com/paper-trail-gem/paper_trail#6b-custom-serializer)
This commit is contained in:
Jared Beck 2021-04-05 17:17:09 -04:00
parent 15a018f669
commit 4ef8a0bfed
10 changed files with 141 additions and 189 deletions

100
README.md
View File

@ -704,42 +704,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')
```
See also:
- `where_object_changes_from`
- `where_object_changes_to`
- `where_attribute_changes`
Using `where_object_changes*` or `where_attribute_changes` to read YAML or JSON
from a text column was deprecated in 8.1.0, and will now raise an error. Use a
`json` or `jsonb` column if possible. If you must use a `text` column, you'll
have to write a custom `object_changes_adapter`.
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],
@ -760,11 +738,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:
@ -800,6 +779,30 @@ 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).
## 4. Saving More Information About Versions
### 4.a. Finding Out Who Was Responsible For A Change
@ -1097,10 +1100,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.
```
@ -1365,17 +1370,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).
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.
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 what your adapter does, you may have to implement all three.
Depending on your needs, you may choose to implement only a subset of these
methods.
For an example of a complete and useful adapter, see
[paper_trail-hashdiff](https://github.com/hashwin/paper_trail-hashdiff)
#### Known Adapters
- [paper_trail-hashdiff](https://github.com/hashwin/paper_trail-hashdiff)
### 6.d. Excluding the Object Column

View File

@ -10,4 +10,24 @@ module PaperTrail
# @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

View File

@ -23,12 +23,16 @@ module PaperTrail
@version_model_class, @attribute
)
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_attribute_changes",
expected: "json or jsonb",
actual: column_type
)
end
end
@ -40,15 +44,6 @@ module PaperTrail
@version_model_class.where(sql, @attribute)
end
# @api private
def text
arel_field = @version_model_class.arel_table[:object_changes]
@version_model_class.where(
::PaperTrail.serializer.where_attribute_changes(arel_field, @attribute)
)
end
end
end
end

View File

@ -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

View File

@ -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

View File

@ -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_to",
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_to_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

View File

@ -14,14 +14,6 @@ module PaperTrail
ActiveSupport::JSON.encode object
end
# Raises an exception as this operation is not allowed from text columns.
def where_attribute_changes(*)
raise Error, <<-STR.squish.freeze
where_attribute_changes does not support reading JSON from a text
column. The json and jsonb datatypes are supported.
STR
end
# Returns a SQL LIKE condition to be used to match the given field and
# value in the serialized object.
def where_object_condition(arel_field, field, value)
@ -39,32 +31,6 @@ module PaperTrail
arel_field.matches("%\"#{field}\":#{json_value}%")
end
end
def where_object_changes_condition(*)
raise Error, <<-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 Error, <<-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
# Raises an exception as this operation is not allowed from text columns.
def where_object_changes_to_condition(*)
raise Error, <<-STR.squish.freeze
where_object_changes_to does not support reading JSON from a text
column. The json and jsonb datatypes are supported.
STR
end
end
end
end

View File

@ -21,47 +21,11 @@ module PaperTrail
::YAML.dump object
end
# Raises an exception as this operation is not allowed from text columns.
def where_attribute_changes(*)
raise Error, <<-STR.squish.freeze
where_attribute_changes does not support reading YAML from a text
column. The json and jsonb datatypes are supported.
STR
end
# Returns a SQL LIKE condition to be used to match the given field and
# value in the serialized object.
def where_object_condition(arel_field, field, value)
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 Error, <<-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
end
# Raises an exception as this operation is not allowed with YAML.
def where_object_changes_from_condition(*)
raise Error, <<-STR.squish.freeze
where_object_changes_from does not support reading YAML from a text
column. The json and jsonb datatypes are supported.
STR
end
# Raises an exception as this operation is not allowed with YAML.
def where_object_changes_to_condition(*)
raise Error, <<-STR.squish.freeze
where_object_changes_to does not support reading YAML from a text
column. The json and jsonb datatypes are supported.
STR
end
end
end
end

View File

@ -4,7 +4,7 @@ require "spec_helper"
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 }
@ -190,9 +190,12 @@ module PaperTrail
bicycle.versions.where_attribute_changes(:name)
).to match_array([bicycle.versions[0], bicycle.versions[1]])
else
expect do
expect {
bicycle.versions.where_attribute_changes(:name)
end.to raise_error(/does not support reading YAML/)
}.to raise_error(
UnsupportedColumnType,
"where_attribute_changes expected json or jsonb column, got text"
)
end
end
end
@ -218,7 +221,10 @@ module PaperTrail
it "raises error" do
expect {
widget.versions.where_attribute_changes(:name).to_a
}.to(raise_error(/does not support reading YAML from a text column/))
}.to raise_error(
UnsupportedColumnType,
"where_attribute_changes expected json or jsonb column, got text"
)
end
end
end
@ -311,9 +317,12 @@ module PaperTrail
bicycle.versions.where_object_changes(name: "abc")
).to match_array(bicycle.versions[0..1])
else
expect do
expect {
bicycle.versions.where_object_changes(name: "abc")
end.to raise_error(/no longer supports reading YAML/)
}.to raise_error(
UnsupportedColumnType,
"where_object_changes expected json or jsonb column, got text"
)
end
end
end
@ -342,7 +351,10 @@ module PaperTrail
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/))
}.to raise_error(
UnsupportedColumnType,
"where_object_changes expected json or jsonb column, got text"
)
end
end
end
@ -389,9 +401,12 @@ module PaperTrail
bicycle.versions.where_object_changes_from(name: "abc")
).to match_array([bicycle.versions[1]])
else
expect do
expect {
bicycle.versions.where_object_changes_from(name: "abc")
end.to raise_error(/does not support reading YAML/)
}.to raise_error(
UnsupportedColumnType,
"where_object_changes_from expected json or jsonb column, got text"
)
end
end
end
@ -421,7 +436,10 @@ module PaperTrail
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/))
}.to raise_error(
UnsupportedColumnType,
"where_object_changes_from expected json or jsonb column, got text"
)
end
end
end
@ -468,9 +486,12 @@ module PaperTrail
bicycle.versions.where_object_changes_to(name: "xyz")
).to match_array([bicycle.versions[1]])
else
expect do
expect {
bicycle.versions.where_object_changes_to(name: "xyz")
end.to raise_error(/does not support reading YAML/)
}.to raise_error(
UnsupportedColumnType,
"where_object_changes_to expected json or jsonb column, got text"
)
end
end
end
@ -503,7 +524,10 @@ module PaperTrail
it "raises error" do
expect {
widget.versions.where_object_changes_to(name: "foo").to_a
}.to(raise_error(/does not support reading YAML from a text column/))
}.to raise_error(
UnsupportedColumnType,
"where_object_changes_to expected json or jsonb column, got text"
)
end
end
end

View File

@ -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