diff options
Diffstat (limited to 'activerecord')
9 files changed, 96 insertions, 27 deletions
diff --git a/activerecord/CHANGELOG.md b/activerecord/CHANGELOG.md index 7cae105ddb..f73e27b91f 100644 --- a/activerecord/CHANGELOG.md +++ b/activerecord/CHANGELOG.md @@ -1,3 +1,23 @@ +* PostgreSQL `tsrange` now preserves subsecond precision + + PostgreSQL 9.1+ introduced range types, and Rails added support for using + this datatype in ActiveRecord. However, the serialization of + PostgreSQL::OID::Range was incomplete, because it did not properly + cast the bounds that make up the range. This led to subseconds being + dropped in SQL commands: + + (byebug) from = type_cast_single_for_database(range.first) + 2010-01-01 13:30:00 UTC + + (byebug) to = type_cast_single_for_database(range.last) + 2011-02-02 19:30:00 UTC + + (byebug) "[#{from},#{to}#{value.exclude_end? ? ')' : ']'}" + "[2010-01-01 13:30:00 UTC,2011-02-02 19:30:00 UTC)" + + (byebug) "[#{type_cast(from)},#{type_cast(to)}#{value.exclude_end? ? ')' : ']'}" + "['2010-01-01 13:30:00.670277','2011-02-02 19:30:00.745125')" + * Passing a `Set` to `Relation#where` now behaves the same as passing an array. diff --git a/activerecord/lib/active_record/attribute_methods/dirty.rb b/activerecord/lib/active_record/attribute_methods/dirty.rb index 4f957ac3ca..06598439d8 100644 --- a/activerecord/lib/active_record/attribute_methods/dirty.rb +++ b/activerecord/lib/active_record/attribute_methods/dirty.rb @@ -34,7 +34,7 @@ module ActiveRecord def reload(*) super.tap do @mutations_before_last_save = nil - clear_mutation_trackers + @mutations_from_database = nil @changed_attributes = ActiveSupport::HashWithIndifferentAccess.new end end @@ -44,22 +44,21 @@ module ActiveRecord @attributes = self.class._default_attributes.map do |attr| attr.with_value_from_user(@attributes.fetch_value(attr.name)) end - clear_mutation_trackers + @mutations_from_database = nil end def changes_applied # :nodoc: - @mutations_before_last_save = mutation_tracker - @mutations_from_database = AttributeMutationTracker.new(@attributes) + @mutations_before_last_save = mutations_from_database @changed_attributes = ActiveSupport::HashWithIndifferentAccess.new forget_attribute_assignments - clear_mutation_trackers + @mutations_from_database = nil end def clear_changes_information # :nodoc: @mutations_before_last_save = nil @changed_attributes = ActiveSupport::HashWithIndifferentAccess.new forget_attribute_assignments - clear_mutation_trackers + @mutations_from_database = nil end def clear_attribute_changes(attr_names) # :nodoc: @@ -75,7 +74,7 @@ module ActiveRecord if defined?(@cached_changed_attributes) @cached_changed_attributes else - super.reverse_merge(mutation_tracker.changed_values).freeze + super.reverse_merge(mutations_from_database.changed_values).freeze end end @@ -90,7 +89,7 @@ module ActiveRecord end def attribute_changed_in_place?(attr_name) # :nodoc: - mutation_tracker.changed_in_place?(attr_name) + mutations_from_database.changed_in_place?(attr_name) end # Did this attribute change when we last saved? This method can be invoked @@ -183,26 +182,18 @@ module ActiveRecord result end - def mutation_tracker - unless defined?(@mutation_tracker) - @mutation_tracker = nil - end - @mutation_tracker ||= AttributeMutationTracker.new(@attributes) - end - def mutations_from_database unless defined?(@mutations_from_database) @mutations_from_database = nil end - @mutations_from_database ||= mutation_tracker + @mutations_from_database ||= AttributeMutationTracker.new(@attributes) end def changes_include?(attr_name) - super || mutation_tracker.changed?(attr_name) + super || mutations_from_database.changed?(attr_name) end def clear_attribute_change(attr_name) - mutation_tracker.forget_change(attr_name) mutations_from_database.forget_change(attr_name) end @@ -227,11 +218,6 @@ module ActiveRecord @attributes = @attributes.map(&:forgetting_assignment) end - def clear_mutation_trackers - @mutation_tracker = nil - @mutations_from_database = nil - end - def mutations_before_last_save @mutations_before_last_save ||= NullMutationTracker.instance end diff --git a/activerecord/lib/active_record/connection_adapters/postgresql/oid/range.rb b/activerecord/lib/active_record/connection_adapters/postgresql/oid/range.rb index 7d5d7d91e6..a89aa5ea09 100644 --- a/activerecord/lib/active_record/connection_adapters/postgresql/oid/range.rb +++ b/activerecord/lib/active_record/connection_adapters/postgresql/oid/range.rb @@ -35,7 +35,7 @@ module ActiveRecord if value.is_a?(::Range) from = type_cast_single_for_database(value.begin) to = type_cast_single_for_database(value.end) - "[#{from},#{to}#{value.exclude_end? ? ')' : ']'}" + ::Range.new(from, to, value.exclude_end?) else super end diff --git a/activerecord/lib/active_record/connection_adapters/postgresql/quoting.rb b/activerecord/lib/active_record/connection_adapters/postgresql/quoting.rb index a0a22ba0f1..9fdeab06c1 100644 --- a/activerecord/lib/active_record/connection_adapters/postgresql/quoting.rb +++ b/activerecord/lib/active_record/connection_adapters/postgresql/quoting.rb @@ -101,6 +101,8 @@ module ActiveRecord end when OID::Array::Data _quote(encode_array(value)) + when Range + _quote(encode_range(value)) else super end @@ -117,6 +119,8 @@ module ActiveRecord value.to_s when OID::Array::Data encode_array(value) + when Range + encode_range(value) else super end @@ -133,6 +137,10 @@ module ActiveRecord result end + def encode_range(range) + "[#{type_cast(range.first)},#{type_cast(range.last)}#{range.exclude_end? ? ')' : ']'}" + end + def determine_encoding_of_strings_in_array(value) case value when ::Array then determine_encoding_of_strings_in_array(value.first) diff --git a/activerecord/lib/active_record/persistence.rb b/activerecord/lib/active_record/persistence.rb index b48a137a73..a57c60ffac 100644 --- a/activerecord/lib/active_record/persistence.rb +++ b/activerecord/lib/active_record/persistence.rb @@ -322,7 +322,7 @@ module ActiveRecord def becomes(klass) became = klass.new became.instance_variable_set("@attributes", @attributes) - became.instance_variable_set("@mutation_tracker", @mutation_tracker) if defined?(@mutation_tracker) + became.instance_variable_set("@mutations_from_database", @mutations_from_database) if defined?(@mutations_from_database) became.instance_variable_set("@changed_attributes", attributes_changed_by_setter) became.instance_variable_set("@new_record", new_record?) became.instance_variable_set("@destroyed", destroyed?) diff --git a/activerecord/test/cases/adapters/postgresql/range_test.rb b/activerecord/test/cases/adapters/postgresql/range_test.rb index b4a776d04d..a75fdef698 100644 --- a/activerecord/test/cases/adapters/postgresql/range_test.rb +++ b/activerecord/test/cases/adapters/postgresql/range_test.rb @@ -232,6 +232,57 @@ _SQL end end + def test_create_tstzrange_preserve_usec + tstzrange = Time.parse("2010-01-01 14:30:00.670277 +0100")...Time.parse("2011-02-02 14:30:00.745125 CDT") + round_trip(@new_range, :tstz_range, tstzrange) + assert_equal @new_range.tstz_range, tstzrange + assert_equal @new_range.tstz_range, Time.parse("2010-01-01 13:30:00.670277 UTC")...Time.parse("2011-02-02 19:30:00.745125 UTC") + end + + def test_update_tstzrange_preserve_usec + assert_equal_round_trip(@first_range, :tstz_range, + Time.parse("2010-01-01 14:30:00.245124 CDT")...Time.parse("2011-02-02 14:30:00.451274 CET")) + assert_nil_round_trip(@first_range, :tstz_range, + Time.parse("2010-01-01 14:30:00.245124 +0100")...Time.parse("2010-01-01 13:30:00.245124 +0000")) + end + + def test_create_tsrange_preseve_usec + tz = ::ActiveRecord::Base.default_timezone + assert_equal_round_trip(@new_range, :ts_range, + Time.send(tz, 2010, 1, 1, 14, 30, 0, 125435)...Time.send(tz, 2011, 2, 2, 14, 30, 0, 225435)) + end + + def test_update_tsrange_preserve_usec + tz = ::ActiveRecord::Base.default_timezone + assert_equal_round_trip(@first_range, :ts_range, + Time.send(tz, 2010, 1, 1, 14, 30, 0, 142432)...Time.send(tz, 2011, 2, 2, 14, 30, 0, 224242)) + assert_nil_round_trip(@first_range, :ts_range, + Time.send(tz, 2010, 1, 1, 14, 30, 0, 142432)...Time.send(tz, 2010, 1, 1, 14, 30, 0, 142432)) + end + + def test_timezone_awareness_tsrange_preserve_usec + tz = "Pacific Time (US & Canada)" + + in_time_zone tz do + PostgresqlRange.reset_column_information + time_string = "2017-09-26 07:30:59.132451 -0700" + time = Time.zone.parse(time_string) + assert time.usec > 0 + + record = PostgresqlRange.new(ts_range: time_string..time_string) + assert_equal time..time, record.ts_range + assert_equal ActiveSupport::TimeZone[tz], record.ts_range.begin.time_zone + assert_equal time.usec, record.ts_range.begin.usec + + record.save! + record.reload + + assert_equal time..time, record.ts_range + assert_equal ActiveSupport::TimeZone[tz], record.ts_range.begin.time_zone + assert_equal time.usec, record.ts_range.begin.usec + end + end + def test_create_numrange assert_equal_round_trip(@new_range, :num_range, BigDecimal.new("0.5")...BigDecimal.new("1")) diff --git a/activerecord/test/cases/adapters/postgresql/type_lookup_test.rb b/activerecord/test/cases/adapters/postgresql/type_lookup_test.rb index 449023b6eb..8212ed4263 100644 --- a/activerecord/test/cases/adapters/postgresql/type_lookup_test.rb +++ b/activerecord/test/cases/adapters/postgresql/type_lookup_test.rb @@ -30,6 +30,6 @@ class PostgresqlTypeLookupTest < ActiveRecord::PostgreSQLTestCase big_range = 0..123456789123456789 assert_raises(ActiveModel::RangeError) { int_range.serialize(big_range) } - assert_equal "[0,123456789123456789]", bigint_range.serialize(big_range) + assert_equal "[0,123456789123456789]", @connection.type_cast(bigint_range.serialize(big_range)) end end diff --git a/activerecord/test/cases/relations_test.rb b/activerecord/test/cases/relations_test.rb index 6270533dc2..4edaf79e9a 100644 --- a/activerecord/test/cases/relations_test.rb +++ b/activerecord/test/cases/relations_test.rb @@ -1806,6 +1806,10 @@ class RelationTest < ActiveRecord::TestCase assert_equal post, custom_post_relation.joins(:author).where!(title: post.title).take end + test "arel_attribute respects a custom table" do + assert_equal [posts(:welcome)], custom_post_relation.ranked_by_comments.limit_by(1).to_a + end + test "#load" do relation = Post.all assert_queries(1) do diff --git a/activerecord/test/models/post.rb b/activerecord/test/models/post.rb index 4c8e847354..935a11e811 100644 --- a/activerecord/test/models/post.rb +++ b/activerecord/test/models/post.rb @@ -21,7 +21,7 @@ class Post < ActiveRecord::Base scope :containing_the_letter_a, -> { where("body LIKE '%a%'") } scope :titled_with_an_apostrophe, -> { where("title LIKE '%''%'") } - scope :ranked_by_comments, -> { order("comments_count DESC") } + scope :ranked_by_comments, -> { order(arel_attribute(:comments_count).desc) } scope :limit_by, lambda { |l| limit(l) } scope :locked, -> { lock } |