Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Query Versions Where Object Changed To Attributes #1291

Merged
merged 1 commit into from
Apr 5, 2021
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,8 @@ recommendations of [keepachangelog.com](http://keepachangelog.com/).

### Added

- `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.
- `where_object_changes_from` queries for versions where the object's attributes
changed from one set of known values to any other set of values.

Expand Down
1 change: 1 addition & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -1339,6 +1339,7 @@ An adapter can implement any or all of the following methods:
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).

Depending on what your adapter does, you may have to implement all three.

Expand Down
65 changes: 65 additions & 0 deletions lib/paper_trail/queries/versions/where_object_changes_to.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,65 @@
# 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

case @version_model_class.columns_hash["object_changes"].type
when :jsonb, :json
json
else
text
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

# @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
end
8 changes: 8 additions & 0 deletions lib/paper_trail/serializers/json.rb
Original file line number Diff line number Diff line change
Expand Up @@ -49,6 +49,14 @@ def where_object_changes_from_condition(*)
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 <<-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
8 changes: 8 additions & 0 deletions lib/paper_trail/serializers/yaml.rb
Original file line number Diff line number Diff line change
Expand Up @@ -46,6 +46,14 @@ def where_object_changes_from_condition(*)
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 <<-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
16 changes: 16 additions & 0 deletions lib/paper_trail/version_concern.rb
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@
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
Expand Down Expand Up @@ -136,6 +137,21 @@ def where_object_changes_from(args = {})
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
Expand Down
82 changes: 82 additions & 0 deletions spec/models/version_spec.rb
Original file line number Diff line number Diff line change
Expand Up @@ -352,6 +352,88 @@ module PaperTrail
end
end
end

describe "#where_object_changes_to", versioning: true do
it "requires its argument to be a Hash" do
expect {
PaperTrail::Version.where_object_changes_to(:foo)
}.to raise_error(ArgumentError)
expect {
PaperTrail::Version.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 = Bicycle.create!(name: "abc")
bicycle.update!(name: "xyz")

allow(adapter).to(
receive(:where_object_changes_to).with(Version, 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 = Bicycle.create!(name: "abc")
bicycle.update!(name: "xyz")

if column_datatype_override
expect(
bicycle.versions.where_object_changes_to(name: "xyz")
).to match_array([bicycle.versions[1]])
else
expect do
bicycle.versions.where_object_changes_to(name: "xyz")
end.to raise_error(/does not support reading YAML/)
end
end
end

# Only test json and jsonb columns. where_object_changes_to 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)

expect(
widget.versions.where_object_changes_to(name: name)
).to eq([widget.versions[0]])
expect(
widget.versions.where_object_changes_to(an_integer: 100)
).to eq([widget.versions[1]])
expect(
widget.versions.where_object_changes_to(an_integer: int)
).to eq([widget.versions[2]])
expect(
widget.versions.where_object_changes_to(an_integer: 100, name: "foobar")
).to eq([widget.versions[1]])
expect(
widget.versions.where_object_changes_to(an_integer: -1)
).to eq([])
end
else
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/))
end
end
end
end
end
end
Expand Down
4 changes: 4 additions & 0 deletions spec/support/custom_object_changes_adapter.rb
Original file line number Diff line number Diff line change
Expand Up @@ -17,4 +17,8 @@ def where_object_changes(klass, attributes)
def where_object_changes_from(klass, attributes)
klass.where(attributes)
end

def where_object_changes_to(klass, attributes)
klass.where(attributes)
end
end