diff --git a/lib/paper_trail/serializers/json.rb b/lib/paper_trail/serializers/json.rb index a17a52bc..99c260de 100644 --- a/lib/paper_trail/serializers/json.rb +++ b/lib/paper_trail/serializers/json.rb @@ -12,6 +12,25 @@ module PaperTrail def dump(object) ActiveSupport::JSON.encode object end + + # Returns a SQL condition to be used to match the given field and value in + # the serialized object. + def where_object_condition(arel_field, field, value) + # Convert to JSON to handle strings and nulls correctly. + json_value = value.to_json + + # If the value is a number, we need to ensure that we find the next + # character too, which is either `,` or `}`, to ensure that searching + # for the value 12 doesn't yield false positives when the value is + # 123. + if value.is_a? Numeric + arel_field.matches("%\"#{field}\":#{json_value},%"). + or( + arel_field.matches("%\"#{field}\":#{json_value}}%")) + else + arel_field.matches("%\"#{field}\":#{json_value}%") + end + end end end end diff --git a/lib/paper_trail/serializers/yaml.rb b/lib/paper_trail/serializers/yaml.rb index d6e05f4d..f2cb77bd 100644 --- a/lib/paper_trail/serializers/yaml.rb +++ b/lib/paper_trail/serializers/yaml.rb @@ -12,6 +12,12 @@ module PaperTrail def dump(object) ::YAML.dump object end + + # Returns a SQL 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 end end end diff --git a/lib/paper_trail/version_concern.rb b/lib/paper_trail/version_concern.rb index f3d04c9a..376b5c98 100644 --- a/lib/paper_trail/version_concern.rb +++ b/lib/paper_trail/version_concern.rb @@ -68,6 +68,20 @@ module PaperTrail end end + # Performs an attribute search on the serialized object by invoking the + # identically-named method in the serializer being used. + def where_object(**args) + arel_field = arel_table[:object] + + where_conditions = args.map do |field, value| + PaperTrail.serializer.where_object_condition(arel_field, field, value) + end.reduce do |condition1, condition2| + condition1.and(condition2) + end + + where(where_conditions) + end + def primary_key_is_int? @primary_key_is_int ||= columns_hash[primary_key].type == :integer rescue @@ -190,7 +204,7 @@ module PaperTrail def index table = self.class.arel_table unless @index - @index ||= + @index ||= if self.class.primary_key_is_int? sibling_versions.select(table[self.class.primary_key]).order(table[self.class.primary_key].asc).index(self) else diff --git a/test/test_helper.rb b/test/test_helper.rb index c187f74b..c4209108 100644 --- a/test/test_helper.rb +++ b/test/test_helper.rb @@ -11,6 +11,7 @@ end require File.expand_path("../dummy/config/environment.rb", __FILE__) require "rails/test_help" +require 'minitest/mock' require 'shoulda' require 'ffaker' require 'database_cleaner' if using_mysql? diff --git a/test/unit/serializers/json_test.rb b/test/unit/serializers/json_test.rb index 577d0e3c..64be5ba5 100644 --- a/test/unit/serializers/json_test.rb +++ b/test/unit/serializers/json_test.rb @@ -37,4 +37,37 @@ class JSONTest < ActiveSupport::TestCase end end + context '`where_object` class method' do + context "when value is a string" do + should 'construct correct WHERE query' do + sql = PaperTrail::Serializers::JSON.where_object_condition( + PaperTrail::Version.arel_table[:object], :arg1, "Val 1"). + to_sql + + assert sql.include?("LIKE '%\"arg1\":\"Val 1\"%'") + end + end + + context "when value is `null`" do + should 'construct correct WHERE query' do + sql = PaperTrail::Serializers::JSON.where_object_condition( + PaperTrail::Version.arel_table[:object], :arg1, nil). + to_sql + + assert sql.include?("LIKE '%\"arg1\":null%'") + end + end + + context "when value is a number" do + should 'construct correct WHERE query' do + sql = PaperTrail::Serializers::JSON.where_object_condition( + PaperTrail::Version.arel_table[:object], :arg1, -3.5). + to_sql + + assert_equal sql, + "(\"versions\".\"object\" LIKE '%\"arg1\":-3.5,%' OR "\ + "\"versions\".\"object\" LIKE '%\"arg1\":-3.5}%')" + end + end + end end diff --git a/test/unit/serializers/yaml_test.rb b/test/unit/serializers/yaml_test.rb index 7e9d09a7..cc2a3d46 100644 --- a/test/unit/serializers/yaml_test.rb +++ b/test/unit/serializers/yaml_test.rb @@ -37,4 +37,12 @@ class YamlTest < ActiveSupport::TestCase end end + context '`where_object` class method' do + should 'construct correct WHERE query' do + sql = PaperTrail::Serializers::YAML.where_object_condition( + PaperTrail::Version.arel_table[:object], :arg1, "Val 1"). + to_sql + assert sql.include?("LIKE '%\narg1: Val 1\n%'") + end + end end diff --git a/test/unit/version_test.rb b/test/unit/version_test.rb index adbecad8..aa24858c 100644 --- a/test/unit/version_test.rb +++ b/test/unit/version_test.rb @@ -98,4 +98,34 @@ class PaperTrail::VersionTest < ActiveSupport::TestCase end end end + + context "PaperTrail::Version.where_object" do + should "call `where_object` on the serializer" do + # Create some args to fake-query on. + args = { a: 1, b: "2", c: false, d: nil } + arel_field = PaperTrail::Version.arel_table[:object] + + # Create a dummy value for us to return for each condition that can be + # chained together with other conditions with Arel's `and`. + chainable_dummy = arel_field.matches("") + + # Mock a serializer to expect to receive `where_object_condition` with the + # correct args. + serializer = MiniTest::Mock.new + serializer.expect :where_object_condition, chainable_dummy, [arel_field, :a, 1] + serializer.expect :where_object_condition, chainable_dummy, [arel_field, :b, "2"] + serializer.expect :where_object_condition, chainable_dummy, [arel_field, :c, false] + serializer.expect :where_object_condition, chainable_dummy, [arel_field, :d, nil] + + # Stub out PaperTrail.serializer to return our mock, and then make the + # query call. + PaperTrail.stub :serializer, serializer do + PaperTrail::Version.where_object(**args) + end + + # Verify that our serializer mock received the correct + # `where_object_condition` calls. + assert serializer.verify + end + end end