diff --git a/activerecord/CHANGELOG.md b/activerecord/CHANGELOG.md index 5929a6f175..beaf5399ec 100644 --- a/activerecord/CHANGELOG.md +++ b/activerecord/CHANGELOG.md @@ -1,3 +1,35 @@ +* Support `where` with comparison operators (`>`, `>=`, `<`, and `<=`). + + ```ruby + posts = Post.order(:id) + + posts.where("id >": 9).pluck(:id) # => [10, 11] + posts.where("id >=": 9).pluck(:id) # => [9, 10, 11] + posts.where("id <": 3).pluck(:id) # => [1, 2] + posts.where("id <=": 3).pluck(:id) # => [1, 2, 3] + ``` + + From type casting and table/column name resolution's point of view, + `where("create_at >=": time)` is better alternative than `where("create_at >= ?", time)`. + + ```ruby + class Post < ActiveRecord::Base + attribute :created_at, :datetime, precision: 3 + end + + time = Time.now.utc # => 2020-06-24 10:11:12.123456 UTC + + Post.create!(created_at: time) # => # + + # SELECT `posts`.* FROM `posts` WHERE (created_at >= '2020-06-24 10:11:12.123456') + Post.where("created_at >= ?", time) # => [] + + # SELECT `posts`.* FROM `posts` WHERE `posts`.`created_at` >= '2020-06-24 10:11:12.123000' + Post.where("created_at >=": time) # => [#] + ``` + + *Ryuta Kamizono* + * Deprecate YAML loading from legacy format older than Rails 5.0. *Ryuta Kamizono* diff --git a/activerecord/lib/active_record/relation/predicate_builder.rb b/activerecord/lib/active_record/relation/predicate_builder.rb index 4452ded7fb..85d7f515e0 100644 --- a/activerecord/lib/active_record/relation/predicate_builder.rb +++ b/activerecord/lib/active_record/relation/predicate_builder.rb @@ -57,11 +57,11 @@ module ActiveRecord @handlers.unshift([klass, handler]) end - def build(attribute, value) + def build(attribute, value, operator = nil) value = value.id if value.is_a?(Base) - if table.type(attribute.name).force_equality?(value) + if operator ||= table.type(attribute.name).force_equality?(value) && :eq bind = build_bind_attribute(attribute.name, value) - attribute.eq(bind) + attribute.public_send(operator, bind) else handler_for(value).call(attribute, value) end @@ -127,12 +127,16 @@ module ActiveRecord grouping_queries(queries) end + elsif key.end_with?(">", ">=", "<", "<=") && /\A(?.+?)\s*(?>|>=|<|<=)\z/ =~ key + build(table.arel_attribute(key), value, OPERATORS[-operator]) else build(table.arel_attribute(key), value) end end end + OPERATORS = { ">" => :gt, ">=" => :gteq, "<" => :lt, "<=" => :lteq }.freeze + private attr_reader :table diff --git a/activerecord/test/cases/date_time_precision_test.rb b/activerecord/test/cases/date_time_precision_test.rb index 001b633676..801ab5b0c3 100644 --- a/activerecord/test/cases/date_time_precision_test.rb +++ b/activerecord/test/cases/date_time_precision_test.rb @@ -94,10 +94,16 @@ if supports_datetime_with_precision? t.datetime :created_at, precision: 0 t.datetime :updated_at, precision: 4 end + date = ::Time.utc(2014, 8, 17, 12, 30, 0, 999999) Foo.create!(created_at: date, updated_at: date) - assert foo = Foo.find_by(created_at: date) - assert_equal 1, Foo.where(updated_at: date).count + + assert_nil Foo.find_by("created_at >= ?", date) + assert_equal 0, Foo.where("updated_at >= ?", date).count + + assert foo = Foo.find_by("created_at >=": date) + assert_equal 1, Foo.where("updated_at >=": date).count + assert_equal date.to_s, foo.created_at.to_s assert_equal date.to_s, foo.updated_at.to_s assert_equal 000000, foo.created_at.usec diff --git a/activerecord/test/cases/relations_test.rb b/activerecord/test/cases/relations_test.rb index 6054ec7503..76108aa38d 100644 --- a/activerecord/test/cases/relations_test.rb +++ b/activerecord/test/cases/relations_test.rb @@ -967,7 +967,7 @@ class RelationTest < ActiveRecord::TestCase assert_equal 11, posts.count(:all) assert_equal 11, posts.count(:id) - assert_equal 3, posts.where("comments_count > 1").count + assert_equal 3, posts.where("comments_count >": 1).count assert_equal 6, posts.where(comments_count: 0).count end @@ -1087,7 +1087,7 @@ class RelationTest < ActiveRecord::TestCase end def test_count_complex_chained_relations - posts = Post.select("comments_count").where("id is not null").group("author_id").where("comments_count > 0") + posts = Post.select("comments_count").where("id is not null").group("author_id").where("comments_count >": 0) expected = { 1 => 4, 2 => 1 } assert_equal expected, posts.count @@ -1109,7 +1109,7 @@ class RelationTest < ActiveRecord::TestCase end def test_empty_complex_chained_relations - posts = Post.select("comments_count").where("id is not null").group("author_id").where("comments_count > 0") + posts = Post.select("comments_count").where("id is not null").group("author_id").where("comments_count >": 0) assert_queries(1) { assert_equal false, posts.empty? } assert_not_predicate posts, :loaded? @@ -2153,6 +2153,25 @@ class RelationTest < ActiveRecord::TestCase assert_not_same first_post, third_post end + def test_where_with_comparison_operator + posts = Post.order(:id) + + assert_equal [10, 11], posts.where("id >": 9).pluck(:id) + assert_equal [9, 10, 11], posts.where("id >=": 9).pluck(:id) + assert_equal [1, 2], posts.where("id <": 3).pluck(:id) + assert_equal [1, 2, 3], posts.where("id <=": 3).pluck(:id) + end + + def test_where_with_table_name_resolution + posts = Post.joins(:comments).order(:id) + + assert_equal [1, 1, 2], posts.where("id <": 3).pluck(:id) + + assert_raise(ActiveRecord::StatementInvalid) do + posts.where("id < ?", 3).pluck(:id) # ambiguous column name: id + end + end + test "#skip_query_cache!" do Post.cache do assert_queries(1) do diff --git a/activerecord/test/cases/scoping/default_scoping_test.rb b/activerecord/test/cases/scoping/default_scoping_test.rb index 5e9f97cb3a..5a2d3a5182 100644 --- a/activerecord/test/cases/scoping/default_scoping_test.rb +++ b/activerecord/test/cases/scoping/default_scoping_test.rb @@ -166,10 +166,10 @@ class DefaultScopingTest < ActiveRecord::TestCase end def test_unscope_string_where_clauses_involved - dev_relation = Developer.order("salary DESC").where("legacy_created_at > ?", 1.year.ago) + dev_relation = Developer.order("salary DESC").where("created_at >": 1.year.ago) expected = dev_relation.collect(&:name) - dev_ordered_relation = DeveloperOrderedBySalary.where(name: "Jamis").where("legacy_created_at > ?", 1.year.ago) + dev_ordered_relation = DeveloperOrderedBySalary.where(name: "Jamis").where("created_at >": 1.year.ago) received = dev_ordered_relation.unscope(where: [:name]).collect(&:name) assert_equal expected.sort, received.sort diff --git a/activerecord/test/cases/time_precision_test.rb b/activerecord/test/cases/time_precision_test.rb index 967037e6c9..ab3d998b2e 100644 --- a/activerecord/test/cases/time_precision_test.rb +++ b/activerecord/test/cases/time_precision_test.rb @@ -88,10 +88,16 @@ if supports_datetime_with_precision? t.time :start, precision: 0 t.time :finish, precision: 4 end + time = ::Time.utc(2000, 1, 1, 12, 30, 0, 999999) Foo.create!(start: time, finish: time) - assert foo = Foo.find_by(start: time) - assert_equal 1, Foo.where(finish: time).count + + assert_nil Foo.find_by("start >= ?", time) + assert_equal 0, Foo.where("finish >= ?", time).count + + assert foo = Foo.find_by("start >=": time) + assert_equal 1, Foo.where("finish >=": time).count + assert_equal time.to_s, foo.start.to_s assert_equal time.to_s, foo.finish.to_s assert_equal 000000, foo.start.usec diff --git a/activerecord/test/models/developer.rb b/activerecord/test/models/developer.rb index 2a8fbe5e4e..d239b629bd 100644 --- a/activerecord/test/models/developer.rb +++ b/activerecord/test/models/developer.rb @@ -156,6 +156,8 @@ class DeveloperFilteredOnJoins < ActiveRecord::Base end class DeveloperOrderedBySalary < ActiveRecord::Base + include Developer::TimestampAliases + self.table_name = "developers" default_scope { order("salary DESC") }