diff --git a/README.md b/README.md index db45b18f..a48f61a2 100644 --- a/README.md +++ b/README.md @@ -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 diff --git a/lib/paper_trail/errors.rb b/lib/paper_trail/errors.rb index 4232db32..0d3f60a7 100644 --- a/lib/paper_trail/errors.rb +++ b/lib/paper_trail/errors.rb @@ -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 diff --git a/lib/paper_trail/queries/versions/where_attribute_changes.rb b/lib/paper_trail/queries/versions/where_attribute_changes.rb index d0eba465..6c875909 100644 --- a/lib/paper_trail/queries/versions/where_attribute_changes.rb +++ b/lib/paper_trail/queries/versions/where_attribute_changes.rb @@ -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 diff --git a/lib/paper_trail/queries/versions/where_object_changes.rb b/lib/paper_trail/queries/versions/where_object_changes.rb index d0de325d..5c08f29a 100644 --- a/lib/paper_trail/queries/versions/where_object_changes.rb +++ b/lib/paper_trail/queries/versions/where_object_changes.rb @@ -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 e3d817b3..13aaf09f 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 index c7b7e979..2c9a09b1 100644 --- a/lib/paper_trail/queries/versions/where_object_changes_to.rb +++ b/lib/paper_trail/queries/versions/where_object_changes_to.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_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 diff --git a/lib/paper_trail/serializers/json.rb b/lib/paper_trail/serializers/json.rb index 521f637a..f0cbf890 100644 --- a/lib/paper_trail/serializers/json.rb +++ b/lib/paper_trail/serializers/json.rb @@ -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 diff --git a/lib/paper_trail/serializers/yaml.rb b/lib/paper_trail/serializers/yaml.rb index 7622a9ff..31ffc9d8 100644 --- a/lib/paper_trail/serializers/yaml.rb +++ b/lib/paper_trail/serializers/yaml.rb @@ -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 diff --git a/spec/models/version_spec.rb b/spec/models/version_spec.rb index 872e89ed..c69556ba 100644 --- a/spec/models/version_spec.rb +++ b/spec/models/version_spec.rb @@ -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 diff --git a/spec/paper_trail/serializers/json_spec.rb b/spec/paper_trail/serializers/json_spec.rb index 2608369e..b8f0fcd3 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