diff options
Diffstat (limited to 'activerecord/lib/active_record/relation')
11 files changed, 162 insertions, 51 deletions
diff --git a/activerecord/lib/active_record/relation/batches.rb b/activerecord/lib/active_record/relation/batches.rb index 8f2dae3369..b99807adf3 100644 --- a/activerecord/lib/active_record/relation/batches.rb +++ b/activerecord/lib/active_record/relation/batches.rb @@ -197,7 +197,7 @@ module ActiveRecord loop do if load - records = batch_relation.to_a + records = batch_relation.records ids = records.map(&:id) yielded_relation = self.where(primary_key => ids) yielded_relation.load_records(records) @@ -214,15 +214,15 @@ module ActiveRecord yield yielded_relation break if ids.length < of - batch_relation = relation.where(table[primary_key].gt(primary_key_offset)) + batch_relation = relation.where(arel_attribute(primary_key).gt(primary_key_offset)) end end private 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 = relation.where(arel_attribute(primary_key).gteq(start)) if start + relation = relation.where(arel_attribute(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 c6e39814dd..13393dc605 100644 --- a/activerecord/lib/active_record/relation/batches/batch_enumerator.rb +++ b/activerecord/lib/active_record/relation/batches/batch_enumerator.rb @@ -35,7 +35,7 @@ module ActiveRecord return to_enum(:each_record) unless block_given? @relation.to_enum(:in_batches, of: @of, start: @start, finish: @finish, load: true).each do |relation| - relation.to_a.each { |record| yield record } + relation.records.each { |record| yield record } end end diff --git a/activerecord/lib/active_record/relation/calculations.rb b/activerecord/lib/active_record/relation/calculations.rb index f45844a9ea..54c9af4898 100644 --- a/activerecord/lib/active_record/relation/calculations.rb +++ b/activerecord/lib/active_record/relation/calculations.rb @@ -155,15 +155,7 @@ module ActiveRecord # See also #ids. # def pluck(*column_names) - column_names.map! do |column_name| - if column_name.is_a?(Symbol) && attribute_alias?(column_name) - attribute_alias(column_name) - else - column_name.to_s - end - end - - if loaded? && (column_names - @klass.column_names).empty? + if loaded? && (column_names.map(&:to_s) - @klass.attribute_names - @klass.attribute_aliases.keys).empty? return @records.pluck(*column_names) end @@ -172,7 +164,7 @@ module ActiveRecord else relation = spawn relation.select_values = column_names.map { |cn| - columns_hash.key?(cn) ? arel_table[cn] : cn + @klass.has_attribute?(cn) || @klass.attribute_alias?(cn) ? arel_attribute(cn) : cn } result = klass.connection.select_all(relation.arel, nil, bound_attributes) result.cast_values(klass.attribute_types) diff --git a/activerecord/lib/active_record/relation/delegation.rb b/activerecord/lib/active_record/relation/delegation.rb index e4e5d63006..f2578f5f96 100644 --- a/activerecord/lib/active_record/relation/delegation.rb +++ b/activerecord/lib/active_record/relation/delegation.rb @@ -37,7 +37,8 @@ module ActiveRecord # for each different klass, and the delegations are compiled into that subclass only. delegate :to_xml, :to_yaml, :length, :collect, :map, :each, :all?, :include?, :to_ary, :join, - :[], :&, :|, :+, :-, :sample, :shuffle, :reverse, :compact, to: :to_a + :[], :&, :|, :+, :-, :sample, :reverse, :compact, :in_groups, :in_groups_of, + :shuffle, :split, to: :records delegate :table_name, :quoted_table_name, :primary_key, :quoted_primary_key, :connection, :columns_hash, :to => :klass diff --git a/activerecord/lib/active_record/relation/finder_methods.rb b/activerecord/lib/active_record/relation/finder_methods.rb index 3f5d6de78a..c3053f0b13 100644 --- a/activerecord/lib/active_record/relation/finder_methods.rb +++ b/activerecord/lib/active_record/relation/finder_methods.rb @@ -145,15 +145,21 @@ module ActiveRecord # # [#<Person id:4>, #<Person id:3>, #<Person id:2>] def last(limit = nil) - if limit - if order_values.empty? && primary_key - order(arel_table[primary_key].desc).limit(limit).reverse - else - to_a.last(limit) - end - else - find_last - end + return find_last(limit) if loaded? || limit_value + + result = limit(limit || 1) + result.order!(arel_attribute(primary_key)) if order_values.empty? && primary_key + result = result.reverse_order! + + limit ? result.reverse : result.first + rescue ActiveRecord::IrreversibleOrderError + ActiveSupport::Deprecation.warn(<<-WARNING.squish) + Finding a last element by loading the relation when SQL ORDER + can not be reversed is deprecated. + Rails 5.1 will raise ActiveRecord::IrreversibleOrderError in this case. + Please call `to_a.last` if you still want to load the relation. + WARNING + find_last(limit) end # Same as #last but raises ActiveRecord::RecordNotFound if no record @@ -242,6 +248,38 @@ module ActiveRecord find_nth! 41 end + # Find the third-to-last record. + # If no order is defined it will order by primary key. + # + # Person.third_to_last # returns the third-to-last object fetched by SELECT * FROM people + # Person.offset(3).third_to_last # returns the third-to-last object from OFFSET 3 + # Person.where(["user_name = :u", { u: user_name }]).third_to_last + def third_to_last + find_nth_from_last 3 + end + + # Same as #third_to_last but raises ActiveRecord::RecordNotFound if no record + # is found. + def third_to_last! + find_nth_from_last 3 or raise RecordNotFound.new("Couldn't find #{@klass.name} with [#{arel.where_sql(@klass.arel_engine)}]") + end + + # Find the second-to-last record. + # If no order is defined it will order by primary key. + # + # Person.second_to_last # returns the second-to-last object fetched by SELECT * FROM people + # Person.offset(3).second_to_last # returns the second-to-last object from OFFSET 3 + # Person.where(["user_name = :u", { u: user_name }]).second_to_last + def second_to_last + find_nth_from_last 2 + end + + # Same as #second_to_last but raises ActiveRecord::RecordNotFound if no record + # is found. + def second_to_last! + find_nth_from_last 2 or raise RecordNotFound.new("Couldn't find #{@klass.name} with [#{arel.where_sql(@klass.arel_engine)}]") + end + # Returns true if a record exists in the table that matches the +id+ or # conditions given, or false otherwise. The argument can take six forms: # @@ -298,7 +336,7 @@ module ActiveRecord end # This method is called whenever no records are found with either a single - # id or multiple ids and raises a ActiveRecord::RecordNotFound exception. + # id or multiple ids and raises an ActiveRecord::RecordNotFound exception. # # The error message is different depending on whether a single id or # multiple ids are provided. If multiple ids are provided, then the number @@ -468,7 +506,7 @@ module ActiveRecord def find_some_ordered(ids) ids = ids.slice(offset_value || 0, limit_value || ids.size) || [] - result = except(:limit, :offset).where(primary_key => ids).to_a + result = except(:limit, :offset).where(primary_key => ids).records if result.size == ids.size pk_type = @klass.type_for_attribute(primary_key) @@ -484,7 +522,7 @@ module ActiveRecord if loaded? @records.first else - @take ||= limit(1).to_a.first + @take ||= limit(1).records.first end end @@ -514,7 +552,7 @@ module ActiveRecord # 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) + order(arel_attribute(primary_key).asc) else self end @@ -523,19 +561,25 @@ module ActiveRecord relation.limit(limit).to_a end - def find_last + def find_nth_from_last(index) if loaded? - @records.last + @records[-index] else - @last ||= - if limit_value - to_a.last - else - reverse_order.limit(1).to_a.first - end + relation = if order_values.empty? && primary_key + order(arel_attribute(primary_key).asc) + else + self + end + + relation.to_a[-index] + # TODO: can be made more performant on large result sets by + # for instance, last(index)[-index] (which would require + # refactoring the last(n) finder method to make test suite pass), + # or by using a combination of reverse_order, limit, and offset, + # e.g., reverse_order.offset(index-1).first end end - + private def find_nth_with_limit_and_offset(index, limit, offset:) # :nodoc: @@ -546,5 +590,9 @@ module ActiveRecord find_nth_with_limit(index, limit) end end + + def find_last(limit) + limit ? records.last(limit) : records.last + end end end diff --git a/activerecord/lib/active_record/relation/predicate_builder.rb b/activerecord/lib/active_record/relation/predicate_builder.rb index 0f88791d92..953495a8b6 100644 --- a/activerecord/lib/active_record/relation/predicate_builder.rb +++ b/activerecord/lib/active_record/relation/predicate_builder.rb @@ -5,6 +5,7 @@ module ActiveRecord require 'active_record/relation/predicate_builder/base_handler' require 'active_record/relation/predicate_builder/basic_object_handler' require 'active_record/relation/predicate_builder/class_handler' + require 'active_record/relation/predicate_builder/polymorphic_array_handler' require 'active_record/relation/predicate_builder/range_handler' require 'active_record/relation/predicate_builder/relation_handler' @@ -22,6 +23,7 @@ module ActiveRecord register_handler(Relation, RelationHandler.new) register_handler(Array, ArrayHandler.new(self)) register_handler(AssociationQueryValue, AssociationQueryHandler.new(self)) + register_handler(PolymorphicArrayValue, PolymorphicArrayHandler.new(self)) end def build_from_hash(attributes) @@ -40,10 +42,7 @@ module ActiveRecord # # For polymorphic relationships, find the foreign key and type: # PriceEstimate.where(estimate_of: treasure) - if table.associated_with?(column) - value = AssociationQueryValue.new(table.associated_table(column), value) - end - + value = AssociationQueryHandler.value_for(table, column, value) if table.associated_with?(column) build(table.arel_attribute(column), value) end diff --git a/activerecord/lib/active_record/relation/predicate_builder/association_query_handler.rb b/activerecord/lib/active_record/relation/predicate_builder/association_query_handler.rb index e81be63cd3..d7fd878265 100644 --- a/activerecord/lib/active_record/relation/predicate_builder/association_query_handler.rb +++ b/activerecord/lib/active_record/relation/predicate_builder/association_query_handler.rb @@ -1,6 +1,16 @@ module ActiveRecord class PredicateBuilder class AssociationQueryHandler # :nodoc: + def self.value_for(table, column, value) + klass = if table.associated_table(column).polymorphic_association? && ::Array === value && value.first.is_a?(Base) + PolymorphicArrayValue + else + AssociationQueryValue + end + + klass.new(table.associated_table(column), value) + end + def initialize(predicate_builder) @predicate_builder = predicate_builder end diff --git a/activerecord/lib/active_record/relation/predicate_builder/polymorphic_array_handler.rb b/activerecord/lib/active_record/relation/predicate_builder/polymorphic_array_handler.rb new file mode 100644 index 0000000000..b6c6240343 --- /dev/null +++ b/activerecord/lib/active_record/relation/predicate_builder/polymorphic_array_handler.rb @@ -0,0 +1,57 @@ +module ActiveRecord + class PredicateBuilder + class PolymorphicArrayHandler # :nodoc: + def initialize(predicate_builder) + @predicate_builder = predicate_builder + end + + def call(attribute, value) + table = value.associated_table + queries = value.type_to_ids_mapping.map do |type, ids| + { table.association_foreign_type.to_s => type, table.association_foreign_key.to_s => ids } + end + + predicates = queries.map { |query| predicate_builder.build_from_hash(query) } + + if predicates.size > 1 + type_and_ids_predicates = predicates.map { |type_predicate, id_predicate| Arel::Nodes::Grouping.new(type_predicate.and(id_predicate)) } + type_and_ids_predicates.inject(&:or) + else + predicates.first + end + end + + protected + + attr_reader :predicate_builder + end + + class PolymorphicArrayValue # :nodoc: + attr_reader :associated_table, :values + + def initialize(associated_table, values) + @associated_table = associated_table + @values = values + end + + def type_to_ids_mapping + default_hash = Hash.new { |hsh, key| hsh[key] = [] } + values.each_with_object(default_hash) { |value, hash| hash[base_class(value).name] << convert_to_id(value) } + end + + private + + def primary_key(value) + associated_table.association_primary_key(base_class(value)) + end + + def base_class(value) + value.class.base_class + end + + def convert_to_id(value) + value._read_attribute(primary_key(value)) + end + end + end +end diff --git a/activerecord/lib/active_record/relation/predicate_builder/relation_handler.rb b/activerecord/lib/active_record/relation/predicate_builder/relation_handler.rb index 063150958a..8a910a82fe 100644 --- a/activerecord/lib/active_record/relation/predicate_builder/relation_handler.rb +++ b/activerecord/lib/active_record/relation/predicate_builder/relation_handler.rb @@ -3,7 +3,7 @@ module ActiveRecord class RelationHandler # :nodoc: def call(attribute, value) if value.select_values.empty? - value = value.select(value.klass.arel_table[value.klass.primary_key]) + value = value.select(value.arel_attribute(value.klass.primary_key)) end attribute.in(value.arel) diff --git a/activerecord/lib/active_record/relation/query_methods.rb b/activerecord/lib/active_record/relation/query_methods.rb index 8ef9f9f627..4533f3263f 100644 --- a/activerecord/lib/active_record/relation/query_methods.rb +++ b/activerecord/lib/active_record/relation/query_methods.rb @@ -655,6 +655,10 @@ module ActiveRecord # # SELECT `posts`.* FROM `posts` WHERE (('id = 1' OR 'author_id = 3')) # def or(other) + unless other.is_a? Relation + raise ArgumentError, "You have passed #{other.class.name} object to #or. Pass an ActiveRecord::Relation object instead." + end + spawn.or!(other) end @@ -1093,8 +1097,8 @@ module ActiveRecord def arel_columns(columns) columns.map do |field| - if (Symbol === field || String === field) && columns_hash.key?(field.to_s) && !from_clause.value - arel_table[field] + if (Symbol === field || String === field) && (klass.has_attribute?(field) || klass.attribute_alias?(field)) && !from_clause.value + arel_attribute(field) elsif Symbol === field connection.quote_table_name(field.to_s) else @@ -1105,13 +1109,15 @@ module ActiveRecord def reverse_sql_order(order_query) if order_query.empty? - return [table[primary_key].desc] if primary_key + return [arel_attribute(primary_key).desc] if primary_key raise IrreversibleOrderError, "Relation has no current order and table has no primary key to be used as default order" end order_query.flat_map do |o| case o + when Arel::Attribute + o.desc when Arel::Nodes::Ordering o.reverse when String @@ -1170,12 +1176,10 @@ module ActiveRecord order_args.map! do |arg| case arg when Symbol - arg = klass.attribute_alias(arg) if klass.attribute_alias?(arg) - table[arg].asc + arel_attribute(arg).asc when Hash arg.map { |field, dir| - field = klass.attribute_alias(field) if klass.attribute_alias?(field) - table[field].send(dir.downcase) + arel_attribute(field).send(dir.downcase) } else arg diff --git a/activerecord/lib/active_record/relation/spawn_methods.rb b/activerecord/lib/active_record/relation/spawn_methods.rb index 67d7f83cb4..d5c18a2a4a 100644 --- a/activerecord/lib/active_record/relation/spawn_methods.rb +++ b/activerecord/lib/active_record/relation/spawn_methods.rb @@ -29,7 +29,7 @@ module ActiveRecord # This is mainly intended for sharing common conditions between multiple associations. def merge(other) if other.is_a?(Array) - to_a & other + records & other elsif other spawn.merge!(other) else |