Skip to content

Commit

Permalink
Attempt to sort based on primary key in scope methods on VersionConce…
Browse files Browse the repository at this point in the history
…rn if primary key is an integer.

This makes some of the changes to these scope methods that came from 6a4aba2 more flexible,
in that the user can choose to compare timestamps if desired, but it defaults to comparing and sorting,
via the primary key (if it is an integer).  If the primary key is not an integer, it still defaults
to using the PaperTrail.timestamp_field.

This is my proposed fix for #314, and I also believe it should fix #317. It seems that that this issue
is usually encountered when testing PaperTrail with MySQL (presumably due to lack of microsecond timestamp support).
  • Loading branch information
batter committed Apr 1, 2014
1 parent 6c9e0b0 commit 202ee99
Show file tree
Hide file tree
Showing 3 changed files with 32 additions and 19 deletions.
2 changes: 1 addition & 1 deletion lib/paper_trail/has_paper_trail.rb
Original file line number Diff line number Diff line change
Expand Up @@ -177,7 +177,7 @@ def originator
def version_at(timestamp, reify_options={})
# Because a version stores how its object looked *before* the change,
# we need to look for the first version created *after* the timestamp.
v = send(self.class.versions_association_name).subsequent(timestamp).first
v = send(self.class.versions_association_name).subsequent(timestamp, true).first
v ? v.reify(reify_options) : self
end

Expand Down
33 changes: 27 additions & 6 deletions lib/paper_trail/version_concern.rb
Original file line number Diff line number Diff line change
Expand Up @@ -33,22 +33,43 @@ def not_creates
where 'event <> ?', 'create'
end

# These methods accept a timestamp or a version and returns other versions that come before or after
def subsequent(obj)
# Expects `obj` to be an instance of `PaperTrail::Version` by default, but can accept a timestamp if
# `timestamp_arg` receives `true`
def subsequent(obj, timestamp_arg = false)
if timestamp_arg != true && self.primary_key_is_int?
return where("#{table_name}.#{self.primary_key} > ?", obj).order("#{table_name}.#{self.primary_key} ASC")

This comment has been minimized.

Copy link
@rposborne

rposborne Apr 2, 2014

Contributor

What would be the use case to ignore the timestamp? Or when would this return something different then the timestamp + id?

Seems like it might just be extra code.

This comment has been minimized.

Copy link
@batter

batter Apr 2, 2014

Author Collaborator

I was trying to literally revert the behavior to look like it did in 6a4aba2. But beyond that, I guess my thought process is, that if you have an auto-incrementing ID, that's always going to be more accurate at giving you the order of creation than a timestamp (especially with what we know about the lack of the microsecond support in most versions of MySQL).

This comment has been minimized.

Copy link
@rposborne

rposborne Apr 2, 2014

Contributor

Right I guess my point is this would give you three possible sorts ["id ASC" , "created_at, id ASC" , "created_at ASC"]. Wouldn't the first two result in same results regardless of millisecond precision?

This comment has been minimized.

Copy link
@batter

batter Apr 2, 2014

Author Collaborator

I guess it should probably be one or the other (id or created_at) now that you mention it, as the first two will give the same result. I was trying to mimic this line's sort order, but it's probably not necessary. I was thinking it would be good to have the option for both for scenarios where you wanted to force a timestamp comparison (such as we do for PaperTrail::Model::InstanceMethods#version_at), but it may not be necessary at all.

end

obj = obj.send(PaperTrail.timestamp_field) if obj.is_a?(self)
where("#{table_name}.#{PaperTrail.timestamp_field} > ?", obj).
order("#{table_name}.#{PaperTrail.timestamp_field} ASC")
order(self.timestamp_sort_order)
end

def preceding(obj)
def preceding(obj, timestamp_arg = false)
if timestamp_arg != true && self.primary_key_is_int?
return where("#{table_name}.#{self.primary_key} < ?", obj).order("#{table_name}.#{self.primary_key} DESC")
end

obj = obj.send(PaperTrail.timestamp_field) if obj.is_a?(self)
where("#{table_name}.#{PaperTrail.timestamp_field} < ?", obj).
order("#{table_name}.#{PaperTrail.timestamp_field} DESC")
order(self.timestamp_sort_order('DESC'))
end


def between(start_time, end_time)
where("#{table_name}.#{PaperTrail.timestamp_field} > ? AND #{table_name}.#{PaperTrail.timestamp_field} < ?",
start_time, end_time).order("#{table_name}.#{PaperTrail.timestamp_field} ASC")
start_time, end_time).order(self.timestamp_sort_order)
end

# defaults to using the primary key as the secondary sort order if possible
def timestamp_sort_order(order = 'ASC')
self.primary_key_is_int? ?

This comment has been minimized.

Copy link
@rposborne

rposborne Apr 2, 2014

Contributor

Shouldn't this be a if else end. I thought it was best practice to only use the ternary operator for single line and very simple if else statements.

This comment has been minimized.

Copy link
@batter

batter Apr 2, 2014

Author Collaborator

Probably makes sense. I cut and pasted it from an in-line order() statement where the if else syntax was not possible.

"#{table_name}.#{PaperTrail.timestamp_field} #{order}, #{table_name}.#{self.primary_key} #{order}" :
"#{table_name}.#{PaperTrail.timestamp_field} #{order}"
end

def primary_key_is_int?
@primary_key_is_int ||= columns_hash[primary_key].type == :integer
end

# Returns whether the `object` column is using the `json` type supported by PostgreSQL
Expand Down
16 changes: 4 additions & 12 deletions test/unit/version_test.rb
Original file line number Diff line number Diff line change
Expand Up @@ -66,20 +66,16 @@ class PaperTrail::VersionTest < ActiveSupport::TestCase

context "receiving a TimeStamp" do
should "return all versions that were created before the Timestamp; descendingly by order of the `PaperTrail.timestamp_field`" do
value = PaperTrail::Version.subsequent(1.hour.ago)
value = PaperTrail::Version.subsequent(1.hour.ago, true)
assert_equal value, @animal.versions.to_a
assert_not_nil value.to_sql.match(/ORDER BY versions.created_at ASC\z/)
assert_not_nil value.to_sql.match(/ORDER BY versions.created_at ASC/)
end
end

context "receiving a `PaperTrail::Version`" do
should "grab the Timestamp from the version and use that as the value" do
value = PaperTrail::Version.subsequent(@animal.versions.first)
assert_equal value, @animal.versions.to_a.tap { |assoc| assoc.shift }
# This asssertion can't pass in Ruby18 because the `strftime` method doesn't accept the %6 (milliseconds) command
if RUBY_VERSION.to_f >= 1.9 and not defined?(JRUBY_VERSION)
assert_not_nil value.to_sql.match(/WHERE \(versions.created_at > '#{@animal.versions.first.send(PaperTrail.timestamp_field).strftime("%F %T.%6N")}'\)/)
end
end
end
end
Expand All @@ -89,20 +85,16 @@ class PaperTrail::VersionTest < ActiveSupport::TestCase

context "receiving a TimeStamp" do
should "return all versions that were created before the Timestamp; descendingly by order of the `PaperTrail.timestamp_field`" do
value = PaperTrail::Version.preceding(Time.now)
value = PaperTrail::Version.preceding(Time.now, true)
assert_equal value, @animal.versions.reverse
assert_not_nil value.to_sql.match(/ORDER BY versions.created_at DESC\z/)
assert_not_nil value.to_sql.match(/ORDER BY versions.created_at DESC/)
end
end

context "receiving a `PaperTrail::Version`" do
should "grab the Timestamp from the version and use that as the value" do
value = PaperTrail::Version.preceding(@animal.versions.last)
assert_equal value, @animal.versions.to_a.tap { |assoc| assoc.pop }.reverse
# This asssertion can't pass in Ruby18 because the `strftime` method doesn't accept the %6 (milliseconds) command
if RUBY_VERSION.to_f >= 1.9 and not defined?(JRUBY_VERSION)
assert_not_nil value.to_sql.match(/WHERE \(versions.created_at < '#{@animal.versions.last.send(PaperTrail.timestamp_field).strftime("%F %T.%6N")}'\)/)
end
end
end
end
Expand Down

0 comments on commit 202ee99

Please sign in to comment.