diff options
Diffstat (limited to 'activerecord/lib/active_record/relation')
11 files changed, 135 insertions, 87 deletions
diff --git a/activerecord/lib/active_record/relation/batches.rb b/activerecord/lib/active_record/relation/batches.rb index 221bc73680..54587ae18e 100644 --- a/activerecord/lib/active_record/relation/batches.rb +++ b/activerecord/lib/active_record/relation/batches.rb @@ -29,15 +29,15 @@ module ActiveRecord # # ==== Options # * <tt>:batch_size</tt> - Specifies the size of the batch. Default to 1000. - # * <tt>:begin_at</tt> - Specifies the primary key value to start from, inclusive of the value. - # * <tt>:end_at</tt> - Specifies the primary key value to end at, inclusive of the value. + # * <tt>:start</tt> - Specifies the primary key value to start from, inclusive of the value. + # * <tt>:finish</tt> - Specifies the primary key value to end at, inclusive of the value. # This is especially useful if you want multiple workers dealing with # the same processing queue. You can make worker 1 handle all the records # between id 0 and 10,000 and worker 2 handle from 10,000 and beyond - # (by setting the +:begin_at+ and +:end_at+ option on each worker). + # (by setting the +:start+ and +:finish+ option on each worker). # # # Let's process for a batch of 2000 records, skipping the first 2000 rows - # Person.find_each(begin_at: 2000, batch_size: 2000) do |person| + # Person.find_each(start: 2000, batch_size: 2000) do |person| # person.party_all_night! # end # @@ -48,22 +48,15 @@ module ActiveRecord # # NOTE: You can't set the limit either, that's used to control # the batch sizes. - def find_each(begin_at: nil, end_at: nil, batch_size: 1000, start: nil) - if start - begin_at = start - ActiveSupport::Deprecation.warn(<<-MSG.squish) - Passing `start` value to find_each is deprecated, and will be removed in Rails 5.1. - Please pass `begin_at` instead. - MSG - end + def find_each(start: nil, finish: nil, batch_size: 1000) if block_given? - find_in_batches(begin_at: begin_at, end_at: end_at, batch_size: batch_size) do |records| + find_in_batches(start: start, finish: finish, batch_size: batch_size) do |records| records.each { |record| yield record } end else - enum_for(:find_each, begin_at: begin_at, end_at: end_at, batch_size: batch_size) do + enum_for(:find_each, start: start, finish: finish, batch_size: batch_size) do relation = self - apply_limits(relation, begin_at, end_at).size + apply_limits(relation, start, finish).size end end end @@ -88,15 +81,15 @@ module ActiveRecord # # ==== Options # * <tt>:batch_size</tt> - Specifies the size of the batch. Default to 1000. - # * <tt>:begin_at</tt> - Specifies the primary key value to start from, inclusive of the value. - # * <tt>:end_at</tt> - Specifies the primary key value to end at, inclusive of the value. + # * <tt>:start</tt> - Specifies the primary key value to start from, inclusive of the value. + # * <tt>:finish</tt> - Specifies the primary key value to end at, inclusive of the value. # This is especially useful if you want multiple workers dealing with # the same processing queue. You can make worker 1 handle all the records # between id 0 and 10,000 and worker 2 handle from 10,000 and beyond - # (by setting the +:begin_at+ and +:end_at+ option on each worker). + # (by setting the +:start+ and +:finish+ option on each worker). # # # Let's process the next 2000 records - # Person.find_in_batches(begin_at: 2000, batch_size: 2000) do |group| + # Person.find_in_batches(start: 2000, batch_size: 2000) do |group| # group.each { |person| person.party_all_night! } # end # @@ -107,24 +100,16 @@ module ActiveRecord # # NOTE: You can't set the limit either, that's used to control # the batch sizes. - def find_in_batches(begin_at: nil, end_at: nil, batch_size: 1000, start: nil) - if start - begin_at = start - ActiveSupport::Deprecation.warn(<<-MSG.squish) - Passing `start` value to find_in_batches is deprecated, and will be removed in Rails 5.1. - Please pass `begin_at` instead. - MSG - end - + def find_in_batches(start: nil, finish: nil, batch_size: 1000) relation = self unless block_given? - return to_enum(:find_in_batches, begin_at: begin_at, end_at: end_at, batch_size: batch_size) do - total = apply_limits(relation, begin_at, end_at).size + return to_enum(:find_in_batches, start: start, finish: finish, batch_size: batch_size) do + total = apply_limits(relation, start, finish).size (total - 1).div(batch_size) + 1 end end - in_batches(of: batch_size, begin_at: begin_at, end_at: end_at, load: true) do |batch| + in_batches(of: batch_size, start: start, finish: finish, load: true) do |batch| yield batch.to_a end end @@ -153,18 +138,18 @@ module ActiveRecord # ==== Options # * <tt>:of</tt> - Specifies the size of the batch. Default to 1000. # * <tt>:load</tt> - Specifies if the relation should be loaded. Default to false. - # * <tt>:begin_at</tt> - Specifies the primary key value to start from, inclusive of the value. - # * <tt>:end_at</tt> - Specifies the primary key value to end at, inclusive of the value. + # * <tt>:start</tt> - Specifies the primary key value to start from, inclusive of the value. + # * <tt>:finish</tt> - Specifies the primary key value to end at, inclusive of the value. # # This is especially useful if you want to work with the # ActiveRecord::Relation object instead of the array of records, or if # you want multiple workers dealing with the same processing queue. You can # make worker 1 handle all the records between id 0 and 10,000 and worker 2 - # handle from 10,000 and beyond (by setting the +:begin_at+ and +:end_at+ + # handle from 10,000 and beyond (by setting the +:start+ and +:finish+ # option on each worker). # # # Let's process the next 2000 records - # Person.in_batches(of: 2000, begin_at: 2000).update_all(awesome: true) + # Person.in_batches(of: 2000, start: 2000).update_all(awesome: true) # # An example of calling where query method on the relation: # @@ -186,10 +171,10 @@ module ActiveRecord # # NOTE: You can't set the limit either, that's used to control the batch # sizes. - def in_batches(of: 1000, begin_at: nil, end_at: nil, load: false) + def in_batches(of: 1000, start: nil, finish: nil, load: false) relation = self unless block_given? - return BatchEnumerator.new(of: of, begin_at: begin_at, end_at: end_at, relation: self) + return BatchEnumerator.new(of: of, start: start, finish: finish, relation: self) end if logger && (arel.orders.present? || arel.taken.present?) @@ -197,7 +182,7 @@ module ActiveRecord end relation = relation.reorder(batch_order).limit(of) - relation = apply_limits(relation, begin_at, end_at) + relation = apply_limits(relation, start, finish) batch_relation = relation loop do @@ -225,9 +210,9 @@ module ActiveRecord private - def apply_limits(relation, begin_at, end_at) - relation = relation.where(table[primary_key].gteq(begin_at)) if begin_at - relation = relation.where(table[primary_key].lteq(end_at)) if end_at + def apply_limits(relation, start, finish) + relation = relation.where(table[primary_key].gteq(start)) if start + relation = relation.where(table[primary_key].lteq(finish)) if finish relation end diff --git a/activerecord/lib/active_record/relation/batches/batch_enumerator.rb b/activerecord/lib/active_record/relation/batches/batch_enumerator.rb index 153aae9584..c6e39814dd 100644 --- a/activerecord/lib/active_record/relation/batches/batch_enumerator.rb +++ b/activerecord/lib/active_record/relation/batches/batch_enumerator.rb @@ -3,11 +3,11 @@ module ActiveRecord class BatchEnumerator include Enumerable - def initialize(of: 1000, begin_at: nil, end_at: nil, relation:) #:nodoc: + def initialize(of: 1000, start: nil, finish: nil, relation:) #:nodoc: @of = of @relation = relation - @begin_at = begin_at - @end_at = end_at + @start = start + @finish = finish end # Looping through a collection of records from the database (using the @@ -34,7 +34,7 @@ module ActiveRecord def each_record return to_enum(:each_record) unless block_given? - @relation.to_enum(:in_batches, of: @of, begin_at: @begin_at, end_at: @end_at, load: true).each do |relation| + @relation.to_enum(:in_batches, of: @of, start: @start, finish: @finish, load: true).each do |relation| relation.to_a.each { |record| yield record } end end @@ -46,7 +46,7 @@ module ActiveRecord # People.in_batches.update_all('age = age + 1') [:delete_all, :update_all, :destroy_all].each do |method| define_method(method) do |*args, &block| - @relation.to_enum(:in_batches, of: @of, begin_at: @begin_at, end_at: @end_at, load: false).each do |relation| + @relation.to_enum(:in_batches, of: @of, start: @start, finish: @finish, load: false).each do |relation| relation.send(method, *args, &block) end end @@ -58,7 +58,7 @@ module ActiveRecord # relation.update_all(awesome: true) # end def each - enum = @relation.to_enum(:in_batches, of: @of, begin_at: @begin_at, end_at: @end_at, load: false) + enum = @relation.to_enum(:in_batches, of: @of, start: @start, finish: @finish, load: false) return enum.each { |relation| yield relation } if block_given? enum end diff --git a/activerecord/lib/active_record/relation/finder_methods.rb b/activerecord/lib/active_record/relation/finder_methods.rb index 19244bcf95..3cbb12a09d 100644 --- a/activerecord/lib/active_record/relation/finder_methods.rb +++ b/activerecord/lib/active_record/relation/finder_methods.rb @@ -117,9 +117,9 @@ module ActiveRecord # def first(limit = nil) if limit - find_nth_with_limit(offset_index, limit) + find_nth_with_limit_and_offset(0, limit, offset: offset_index) else - find_nth(0, offset_index) + find_nth 0 end end @@ -169,7 +169,7 @@ module ActiveRecord # Person.offset(3).second # returns the second object from OFFSET 3 (which is OFFSET 4) # Person.where(["user_name = :u", { u: user_name }]).second def second - find_nth(1, offset_index) + find_nth 1 end # Same as #second but raises ActiveRecord::RecordNotFound if no record @@ -185,7 +185,7 @@ module ActiveRecord # Person.offset(3).third # returns the third object from OFFSET 3 (which is OFFSET 5) # Person.where(["user_name = :u", { u: user_name }]).third def third - find_nth(2, offset_index) + find_nth 2 end # Same as #third but raises ActiveRecord::RecordNotFound if no record @@ -201,7 +201,7 @@ module ActiveRecord # Person.offset(3).fourth # returns the fourth object from OFFSET 3 (which is OFFSET 6) # Person.where(["user_name = :u", { u: user_name }]).fourth def fourth - find_nth(3, offset_index) + find_nth 3 end # Same as #fourth but raises ActiveRecord::RecordNotFound if no record @@ -217,7 +217,7 @@ module ActiveRecord # Person.offset(3).fifth # returns the fifth object from OFFSET 3 (which is OFFSET 7) # Person.where(["user_name = :u", { u: user_name }]).fifth def fifth - find_nth(4, offset_index) + find_nth 4 end # Same as #fifth but raises ActiveRecord::RecordNotFound if no record @@ -233,7 +233,7 @@ module ActiveRecord # Person.offset(3).forty_two # returns the forty-second object from OFFSET 3 (which is OFFSET 44) # Person.where(["user_name = :u", { u: user_name }]).forty_two def forty_two - find_nth(41, offset_index) + find_nth 41 end # Same as #forty_two but raises ActiveRecord::RecordNotFound if no record @@ -488,27 +488,39 @@ module ActiveRecord end end - def find_nth(index, offset) + def find_nth(index, offset = nil) if loaded? @records[index] else - offset += index - @offsets[offset] ||= find_nth_with_limit(offset, 1).first + # TODO: once the offset argument is removed we rely on offset_index + # within find_nth_with_limit, rather than pass it in via + # find_nth_with_limit_and_offset + if offset + ActiveSupport::Deprecation.warn(<<-MSG.squish) + Passing an offset argument to find_nth is deprecated, + please use Relation#offset instead. + MSG + else + offset = offset_index + end + @offsets[offset + index] ||= find_nth_with_limit_and_offset(index, 1, offset: offset).first end end def find_nth!(index) - find_nth(index, offset_index) or raise RecordNotFound.new("Couldn't find #{@klass.name} with [#{arel.where_sql(@klass.arel_engine)}]") + find_nth(index) or raise RecordNotFound.new("Couldn't find #{@klass.name} with [#{arel.where_sql(@klass.arel_engine)}]") end - def find_nth_with_limit(offset, limit) + def find_nth_with_limit(index, limit) + # TODO: once the offset argument is removed from find_nth, + # find_nth_with_limit_and_offset can be merged into this method relation = if order_values.empty? && primary_key order(arel_table[primary_key].asc) else self end - relation = relation.offset(offset) unless offset.zero? + relation = relation.offset(index) unless index.zero? relation.limit(limit).to_a end @@ -524,5 +536,16 @@ module ActiveRecord end end end + + private + + def find_nth_with_limit_and_offset(index, limit, offset:) # :nodoc: + if loaded? + @records[index, limit] + else + index += offset + find_nth_with_limit(index, limit) + end + end end end diff --git a/activerecord/lib/active_record/relation/from_clause.rb b/activerecord/lib/active_record/relation/from_clause.rb index 92340216ed..8945cb0cc5 100644 --- a/activerecord/lib/active_record/relation/from_clause.rb +++ b/activerecord/lib/active_record/relation/from_clause.rb @@ -25,7 +25,7 @@ module ActiveRecord end def self.empty - new(nil, nil) + @empty ||= new(nil, nil) end end end diff --git a/activerecord/lib/active_record/relation/merger.rb b/activerecord/lib/active_record/relation/merger.rb index cb971eb255..396638d74d 100644 --- a/activerecord/lib/active_record/relation/merger.rb +++ b/activerecord/lib/active_record/relation/merger.rb @@ -141,6 +141,9 @@ module ActiveRecord end def merge_single_values + if relation.from_clause.empty? + relation.from_clause = other.from_clause + end relation.lock_value ||= other.lock_value unless other.create_with_value.blank? diff --git a/activerecord/lib/active_record/relation/predicate_builder.rb b/activerecord/lib/active_record/relation/predicate_builder.rb index 39e7b42629..0f88791d92 100644 --- a/activerecord/lib/active_record/relation/predicate_builder.rb +++ b/activerecord/lib/active_record/relation/predicate_builder.rb @@ -18,6 +18,7 @@ module ActiveRecord register_handler(Class, ClassHandler.new(self)) register_handler(Base, BaseHandler.new(self)) register_handler(Range, RangeHandler.new(self)) + register_handler(RangeHandler::RangeWithBinds, RangeHandler.new(self)) register_handler(Relation, RelationHandler.new) register_handler(Array, ArrayHandler.new(self)) register_handler(AssociationQueryValue, AssociationQueryHandler.new(self)) @@ -105,10 +106,23 @@ module ActiveRecord binds += bvs when Relation binds += value.bound_attributes + when Range + first = value.begin + last = value.end + unless first.respond_to?(:infinite?) && first.infinite? + binds << build_bind_param(column_name, first) + first = Arel::Nodes::BindParam.new + end + unless last.respond_to?(:infinite?) && last.infinite? + binds << build_bind_param(column_name, last) + last = Arel::Nodes::BindParam.new + end + + result[column_name] = RangeHandler::RangeWithBinds.new(first, last, value.exclude_end?) else if can_be_bound?(column_name, value) result[column_name] = Arel::Nodes::BindParam.new - binds << Relation::QueryAttribute.new(column_name.to_s, value, table.type(column_name)) + binds << build_bind_param(column_name, value) end end end @@ -145,5 +159,9 @@ module ActiveRecord handler_for(value).is_a?(BasicObjectHandler) && !table.associated_with?(column_name) end + + def build_bind_param(column_name, value) + Relation::QueryAttribute.new(column_name.to_s, value, table.type(column_name)) + end end end diff --git a/activerecord/lib/active_record/relation/predicate_builder/range_handler.rb b/activerecord/lib/active_record/relation/predicate_builder/range_handler.rb index 1b3849e3ad..306d4694ae 100644 --- a/activerecord/lib/active_record/relation/predicate_builder/range_handler.rb +++ b/activerecord/lib/active_record/relation/predicate_builder/range_handler.rb @@ -1,12 +1,28 @@ module ActiveRecord class PredicateBuilder class RangeHandler # :nodoc: + RangeWithBinds = Struct.new(:begin, :end, :exclude_end?) + def initialize(predicate_builder) @predicate_builder = predicate_builder end def call(attribute, value) - attribute.between(value) + if value.begin.respond_to?(:infinite?) && value.begin.infinite? + if value.end.respond_to?(:infinite?) && value.end.infinite? + attribute.not_in([]) + elsif value.exclude_end? + attribute.lt(value.end) + else + attribute.lteq(value.end) + end + elsif value.end.respond_to?(:infinite?) && value.end.infinite? + attribute.gteq(value.begin) + elsif value.exclude_end? + attribute.gteq(value.begin).and(attribute.lt(value.end)) + else + attribute.between(value) + end end protected diff --git a/activerecord/lib/active_record/relation/query_methods.rb b/activerecord/lib/active_record/relation/query_methods.rb index 983bf019bc..716b1e8505 100644 --- a/activerecord/lib/active_record/relation/query_methods.rb +++ b/activerecord/lib/active_record/relation/query_methods.rb @@ -54,16 +54,17 @@ module ActiveRecord end end + FROZEN_EMPTY_ARRAY = [].freeze Relation::MULTI_VALUE_METHODS.each do |name| class_eval <<-CODE, __FILE__, __LINE__ + 1 - def #{name}_values # def select_values - @values[:#{name}] || [] # @values[:select] || [] - end # end - # - def #{name}_values=(values) # def select_values=(values) - assert_mutability! # assert_mutability! - @values[:#{name}] = values # @values[:select] = values - end # end + def #{name}_values + @values[:#{name}] || FROZEN_EMPTY_ARRAY + end + + def #{name}_values=(values) + assert_mutability! + @values[:#{name}] = values + end CODE end @@ -116,8 +117,9 @@ module ActiveRecord result end + FROZEN_EMPTY_HASH = {}.freeze def create_with_value # :nodoc: - @values[:create_with] || {} + @values[:create_with] || FROZEN_EMPTY_HASH end alias extensions extending_values @@ -649,16 +651,18 @@ module ActiveRecord # they must differ only by #where (if no #group has been defined) or #having (if a #group is # present). Neither relation may have a #limit, #offset, or #distinct set. # - # Post.where("id = 1").or(Post.where("id = 2")) - # # SELECT `posts`.* FROM `posts` WHERE (('id = 1' OR 'id = 2')) + # Post.where("id = 1").or(Post.where("author_id = 3")) + # # SELECT `posts`.* FROM `posts` WHERE (('id = 1' OR 'author_id = 3')) # def or(other) spawn.or!(other) end def or!(other) # :nodoc: - unless structurally_compatible_for_or?(other) - raise ArgumentError, 'Relation passed to #or must be structurally compatible' + incompatible_values = structurally_incompatible_values_for_or(other) + + unless incompatible_values.empty? + raise ArgumentError, "Relation passed to #or must be structurally compatible. Incompatible values: #{incompatible_values}" end self.where_clause = self.where_clause.or(other.where_clause) @@ -1187,10 +1191,10 @@ module ActiveRecord end end - def structurally_compatible_for_or?(other) - Relation::SINGLE_VALUE_METHODS.all? { |m| send("#{m}_value") == other.send("#{m}_value") } && - (Relation::MULTI_VALUE_METHODS - [:extending]).all? { |m| send("#{m}_values") == other.send("#{m}_values") } && - (Relation::CLAUSE_METHODS - [:having, :where]).all? { |m| send("#{m}_clause") != other.send("#{m}_clause") } + def structurally_incompatible_values_for_or(other) + Relation::SINGLE_VALUE_METHODS.reject { |m| send("#{m}_value") == other.send("#{m}_value") } + + (Relation::MULTI_VALUE_METHODS - [:extending]).reject { |m| send("#{m}_values") == other.send("#{m}_values") } + + (Relation::CLAUSE_METHODS - [:having, :where]).reject { |m| send("#{m}_clause") == other.send("#{m}_clause") } end def new_where_clause diff --git a/activerecord/lib/active_record/relation/record_fetch_warning.rb b/activerecord/lib/active_record/relation/record_fetch_warning.rb index 0a1814b3dd..dbd08811fa 100644 --- a/activerecord/lib/active_record/relation/record_fetch_warning.rb +++ b/activerecord/lib/active_record/relation/record_fetch_warning.rb @@ -24,9 +24,7 @@ module ActiveRecord end # :stopdoc: - ActiveSupport::Notifications.subscribe("sql.active_record") do |*args| - payload = args.last - + ActiveSupport::Notifications.subscribe("sql.active_record") do |*, payload| QueryRegistry.queries << payload[:sql] end # :startdoc: @@ -34,14 +32,14 @@ module ActiveRecord class QueryRegistry # :nodoc: extend ActiveSupport::PerThreadRegistry - attr_accessor :queries + attr_reader :queries def initialize - reset + @queries = [] end def reset - @queries = [] + @queries.clear end end end diff --git a/activerecord/lib/active_record/relation/where_clause.rb b/activerecord/lib/active_record/relation/where_clause.rb index 1f000b3f0f..2c2d6cfa47 100644 --- a/activerecord/lib/active_record/relation/where_clause.rb +++ b/activerecord/lib/active_record/relation/where_clause.rb @@ -81,7 +81,7 @@ module ActiveRecord end def self.empty - new([], []) + @empty ||= new([], []) end protected diff --git a/activerecord/lib/active_record/relation/where_clause_factory.rb b/activerecord/lib/active_record/relation/where_clause_factory.rb index a81ff98e49..dbf172a577 100644 --- a/activerecord/lib/active_record/relation/where_clause_factory.rb +++ b/activerecord/lib/active_record/relation/where_clause_factory.rb @@ -22,6 +22,7 @@ module ActiveRecord parts = predicate_builder.build_from_hash(attributes) when Arel::Nodes::Node parts = [opts] + binds = other else raise ArgumentError, "Unsupported argument type: #{opts} (#{opts.class})" end |