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 Attribute Changed #1292

Merged
merged 2 commits 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
3 changes: 2 additions & 1 deletion CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,8 @@ recommendations of [keepachangelog.com](http://keepachangelog.com/).

### Added

- None
- `where_attribute_changes` queries for versions where the object's attribute
changed to or from any values.

### Fixed

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

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

case @version_model_class.columns_hash["object_changes"].type
when :jsonb, :json
json
else
text
end
end

private

# @api private
def json
sql = "object_changes -> ? IS NOT NULL"

@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
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 @@ -14,6 +14,14 @@ def dump(object)
ActiveSupport::JSON.encode object
end

# Raises an exception as this operation is not allowed from text columns.
def where_attribute_changes(*)
raise <<-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)
Expand Down
8 changes: 8 additions & 0 deletions lib/paper_trail/serializers/yaml.rb
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,14 @@ def dump(object)
::YAML.dump object
end

# Raises an exception as this operation is not allowed from text columns.
def where_attribute_changes(*)
raise <<-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)
Expand Down
13 changes: 13 additions & 0 deletions lib/paper_trail/version_concern.rb
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
# 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"
Expand Down Expand Up @@ -61,6 +62,18 @@ def timestamp_sort_order(direction = "asc")
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.
#
Expand Down
75 changes: 75 additions & 0 deletions spec/models/version_spec.rb
Original file line number Diff line number Diff line change
Expand Up @@ -148,6 +148,81 @@ module PaperTrail
end
end

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

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

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

# Only test json and jsonb columns. where_attribute_changes does
# not support text columns.
if column_datatype_override
it "locates versions according to their object_changes contents" do
widget.update!(name: "foobar", an_integer: 100)
widget.update!(an_integer: 17)

expect(
widget.versions.where_attribute_changes(:name)
).to eq([widget.versions[0]])
expect(
widget.versions.where_attribute_changes("an_integer")
).to eq([widget.versions[0], widget.versions[1]])
expect(
widget.versions.where_attribute_changes(:a_float)
).to eq([])
end
else
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/))
end
end
end

describe "#where_object", versioning: true do
it "requires its argument to be a Hash" do
widget.update!(name: name, an_integer: int)
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 @@ -10,6 +10,10 @@ def load_changeset(version)
version.changeset
end

def where_attribute_changes(klass, attribute)
klass.where(attribute)
end

def where_object_changes(klass, attributes)
klass.where(attributes)
end
Expand Down