Skip to content

[#52698] Fix TimeZoneConverter#== method so objects will be properly compared by their type, scale, limit & precision. #52699

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

Merged
merged 1 commit into from
Aug 27, 2024

Conversation

ruyrocha
Copy link
Contributor

@ruyrocha ruyrocha commented Aug 24, 2024

Motivation / Background

This Pull Request has been created because the comparison for Timestamp on TimeZoneConverter was misbehaving.

Detail

This Pull Request changes TimeZoneConverter#== so it can correctly check the comparison with other objects.

Fixes #52698

Additional information

Checklist

Before submitting the PR make sure the following are checked:

  • This Pull Request is related to one change. Unrelated changes should be opened in separate PRs.
  • Commit message has a detailed description of what changed and why. If this PR fixes a related issue include it in the commit message. Ex: [Fix #issue-number]
  • Tests are added or updated if you fix a bug or add a feature.
  • CHANGELOG files are updated for the changed libraries if there is a behavior change or additional feature. Minor bug fixes and documentation changes should not be included.

@ruyrocha ruyrocha force-pushed the fix/comparison-of-timestamp-type branch from 450ade7 to 69bce6b Compare August 24, 2024 01:27
@MatheusRich
Copy link
Contributor

@ruyrocha please add a changelog entry for this

@ruyrocha ruyrocha force-pushed the fix/comparison-of-timestamp-type branch 2 times, most recently from 16cdcc2 to 82d8e0a Compare August 24, 2024 13:40
@ruyrocha
Copy link
Contributor Author

@ruyrocha please add a changelog entry for this

Done, @MatheusRich

Copy link
Member

@rafaelfranca rafaelfranca left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thank you for the pull request but I think it is incomplete and probably fixing in the wrong place.

@@ -8,6 +8,13 @@ class Timestamp < DateTime # :nodoc:
def type
real_type_unless_aliased(:timestamp)
end

def ==(other)
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Would not this only fix the problem with the postgresql timestamp? I think the problem happens with any type that uses TimeZoneConverter, which also includes DateTime

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Got it, @rafaelfranca

I'll push a new fix for reviews quite soon

@ruyrocha ruyrocha changed the title [#52698] Fix comparison of Timestamp type for PostgreSQL. [#52698] Fix TimeZoneConverter#== method so objects will be properly compared by their type, scale, limit & precision. Aug 27, 2024
@ruyrocha ruyrocha force-pushed the fix/comparison-of-timestamp-type branch 3 times, most recently from 09578d6 to 7a51b56 Compare August 27, 2024 02:09
…roperly...

compared by their type, scale, limit & precision.
@ruyrocha ruyrocha force-pushed the fix/comparison-of-timestamp-type branch from 7a51b56 to 71e5dc3 Compare August 27, 2024 11:36
@ruyrocha ruyrocha requested a review from rafaelfranca August 27, 2024 12:28
@rafaelfranca rafaelfranca merged commit 7059d3c into rails:main Aug 27, 2024
3 checks passed
rafaelfranca added a commit that referenced this pull request Aug 27, 2024
[#52698] Fix `TimeZoneConverter#==` method so objects will be properly compared by their type, scale, limit & precision.
rafaelfranca added a commit that referenced this pull request Aug 27, 2024
[#52698] Fix `TimeZoneConverter#==` method so objects will be properly compared by their type, scale, limit & precision.
@ruyrocha ruyrocha deleted the fix/comparison-of-timestamp-type branch August 27, 2024 16:24
@@ -32,6 +32,13 @@ def cast(value)
end
end

def ==(other)

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@ruyrocha Hey, thanks for the PR! Just got around to taking a look. Perhaps worth noting that this introduces kind of the opposite problem described by #52698.

Before:

subtype = ActiveRecord::Type::DateTime.new
value = ActiveRecord::AttributeMethods::TimeZoneConversion::TimeZoneConverter.new(subtype)
value == "something that doesn't respond to precision/scale/limit/type"
#=> false

After:

subtype = ActiveRecord::Type::DateTime.new
value = ActiveRecord::AttributeMethods::TimeZoneConversion::TimeZoneConverter.new(subtype)
value == "something that doesn't respond to precision/scale/limit/type"
#=> NoMethodError: undefined method `precision' for an instance of String

I guess there might be arguments about duck typing, but for a method like #==, I'd say it's probably a good idea to be resilient to comparing two completely unrelated values. 🤷 The class check of the original method would catch such problems, but in a way that's too strict for the DelegateClass usage, which results in false negatives. Maybe an other.is_a?(ActiveModel::Type::Value) at the front of this definition would suffice?

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thanks. Does this fix the issue you were having and the new one?

diff --git a/activerecord/lib/active_record/attribute_methods/time_zone_conversion.rb b/activerecord/lib/active_record/attribute_methods/time_zone_conversion.rb
index 27e83ec6dd..26e974d83a 100644
--- a/activerecord/lib/active_record/attribute_methods/time_zone_conversion.rb
+++ b/activerecord/lib/active_record/attribute_methods/time_zone_conversion.rb
@@ -33,10 +33,7 @@ def cast(value)
         end
 
         def ==(other)
-          precision == other.precision &&
-            scale == other.scale &&
-            limit == other.limit &&
-            type == other.type
+          other.is_a?(self.class) && __getobj__ == other.__getobj__
         end
 
         private
diff --git a/activerecord/test/cases/attribute_methods/time_zone_converter_test.rb b/activerecord/test/cases/attribute_methods/time_zone_converter_test.rb
index 5deb026f15..dac23f1ab2 100644
--- a/activerecord/test/cases/attribute_methods/time_zone_converter_test.rb
+++ b/activerecord/test/cases/attribute_methods/time_zone_converter_test.rb
@@ -13,6 +13,7 @@ def test_comparison_with_date_time_type
           value_from_cache = Marshal.load(Marshal.dump(value))
 
           assert_equal value, value_from_cache
+          assert_not_equal value, "foo"
         end
       end
     end

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It looks good, @rafaelfranca, and I think it'll fix the issues @ajvondrak was facing. I'd only put it first to be consistent with other #== methods, e.g:

        def ==(other)
          other.is_a?(self.class) && __getobj__ == other.__getobj__ &&
            precision == other.precision &&
            scale == other.scale &&
            limit == other.limit &&
            type == other.type
        end

I'm happy to push it if you don't have any objections

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Hmm, I suppose other.is_a?(self.class) would technically change another piece of behavior from the old version in the case of comparing a timezone-wrapped datetime to just a raw datetime.

Before:

unwrapped = ActiveRecord::Type::DateTime.new
wrapped = ActiveRecord::AttributeMethods::TimeZoneConversion::TimeZoneConverter.new(unwrapped)

# since wrapped.== delegates to the underlying unwrapped.==, this is equivalent to unwrapped == unwrapped:
wrapped == unwrapped
#=> true

# but unwrapped.== doesn't know to unwrap the wrapped value:
unwrapped == wrapped
#=> false

After:

unwrapped = ActiveRecord::Type::DateTime.new
wrapped = ActiveRecord::AttributeMethods::TimeZoneConversion::TimeZoneConverter.new(unwrapped)

# now, unwrapped.is_a?(TimeZoneConverter) fails:
wrapped == unwrapped
#=> false

# this is unchanged:
unwrapped == wrapped
#=> false

I think the After behavior actually makes more sense, since it keeps == symmetric. So yeah, I can't think of any other gotchas offhand with the is_a? + __getobj__ == __getobj__ check. 👍

@ruyrocha FWIW, I don't think that the precision/scale/limit/type comparisons are necessary with the proposed definition given that we'd be delegating to __getobj__ == other.__getobj__, which will already house its own logic for comparing those attributes.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@ajvondrak @rafaelfranca #52726 is ready, but I don't know how to proceed:

ruyrocha added a commit to ruyrocha/rails that referenced this pull request Aug 28, 2024
…ic, so it can...

properly compare `other` with `itself`, and remove the comparison of
type, scale, limit & precision.
rafaelfranca pushed a commit to ruyrocha/rails that referenced this pull request Aug 28, 2024
…ic, so it can...

properly compare `other` with `itself`, and remove the comparison of
type, scale, limit & precision.
rafaelfranca pushed a commit to ruyrocha/rails that referenced this pull request Aug 28, 2024
…ic, so it can...

properly compare `other` with `itself`, and remove the comparison of
type, scale, limit & precision.
rafaelfranca added a commit that referenced this pull request Aug 28, 2024
…thod

[#52699] Update `TimeZoneConverter#==` method to make it symmetric
rafaelfranca added a commit that referenced this pull request Aug 28, 2024
…thod

[#52699] Update `TimeZoneConverter#==` method to make it symmetric
rafaelfranca added a commit that referenced this pull request Aug 28, 2024
…thod

[#52699] Update `TimeZoneConverter#==` method to make it symmetric
DanielaVelasquez pushed a commit to DanielaVelasquez/rails that referenced this pull request Oct 3, 2024
…ic, so it can...

properly compare `other` with `itself`, and remove the comparison of
type, scale, limit & precision.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Projects
None yet
Development

Successfully merging this pull request may close these issues.

ActiveRecord::AttributeMethods::TimeZoneConversion::TimeZoneConverter equality is buggy
4 participants