diff options
Diffstat (limited to 'activerecord/lib/active_record/relation')
7 files changed, 135 insertions, 53 deletions
diff --git a/activerecord/lib/active_record/relation/calculations.rb b/activerecord/lib/active_record/relation/calculations.rb index 45ffb99868..56cf9bcd27 100644 --- a/activerecord/lib/active_record/relation/calculations.rb +++ b/activerecord/lib/active_record/relation/calculations.rb @@ -19,6 +19,14 @@ module ActiveRecord # # Person.group(:city).count # # => { 'Rome' => 5, 'Paris' => 3 } + # + # If +count+ is used with +select+, it will count the selected columns: + # + # Person.select(:age).count + # # => counts the number of different age values + # + # Note: not all valid +select+ expressions are valid +count+ expressions. The specifics differ + # between databases. In invalid cases, an error from the databsae is thrown. def count(column_name = nil, options = {}) # TODO: Remove options argument as soon we remove support to # activerecord-deprecated_finders. @@ -188,7 +196,7 @@ module ActiveRecord private def has_include?(column_name) - eager_loading? || (includes_values.present? && (column_name || references_eager_loaded_tables?)) + eager_loading? || (includes_values.present? && ((column_name && column_name != :all) || references_eager_loaded_tables?)) end def perform_calculation(operation, column_name, options = {}) @@ -231,15 +239,18 @@ module ActiveRecord def execute_simple_calculation(operation, column_name, distinct) #:nodoc: # Postgresql doesn't like ORDER BY when there are no GROUP BY - relation = reorder(nil) + relation = unscope(:order) column_alias = column_name + bind_values = nil + if operation == "count" && (relation.limit_value || relation.offset_value) # Shortcut when limit is zero. return 0 if relation.limit_value == 0 query_builder = build_count_subquery(relation, column_name, distinct) + bind_values = query_builder.bind_values + relation.bind_values else column = aggregate_column(column_name) @@ -249,9 +260,10 @@ module ActiveRecord relation.select_values = [select_value] query_builder = relation.arel + bind_values = query_builder.bind_values + relation.bind_values end - result = @klass.connection.select_all(query_builder, nil, relation.bind_values) + result = @klass.connection.select_all(query_builder, nil, bind_values) row = result.first value = row && row.values.first column = result.column_types.fetch(column_alias) do @@ -385,9 +397,11 @@ module ActiveRecord aliased_column = aggregate_column(column_name == :all ? 1 : column_name).as(column_alias) relation.select_values = [aliased_column] - subquery = relation.arel.as(subquery_alias) + arel = relation.arel + subquery = arel.as(subquery_alias) sm = Arel::SelectManager.new relation.engine + sm.bind_values = arel.bind_values select_value = operation_over_aggregate_column(column_alias, 'count', distinct) sm.project(select_value).from(subquery) end diff --git a/activerecord/lib/active_record/relation/delegation.rb b/activerecord/lib/active_record/relation/delegation.rb index 21beed332f..50f4d5c7ab 100644 --- a/activerecord/lib/active_record/relation/delegation.rb +++ b/activerecord/lib/active_record/relation/delegation.rb @@ -40,10 +40,10 @@ module ActiveRecord BLACKLISTED_ARRAY_METHODS = [ :compact!, :flatten!, :reject!, :reverse!, :rotate!, :map!, :shuffle!, :slice!, :sort!, :sort_by!, :delete_if, - :keep_if, :pop, :shift, :delete_at, :compact + :keep_if, :pop, :shift, :delete_at, :compact, :select! ].to_set # :nodoc: - delegate :to_xml, :to_yaml, :length, :collect, :map, :each, :all?, :include?, :to_ary, to: :to_a + delegate :to_xml, :to_yaml, :length, :collect, :map, :each, :all?, :include?, :to_ary, :join, to: :to_a 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 c2b9dc08fe..db32ae12a8 100644 --- a/activerecord/lib/active_record/relation/finder_methods.rb +++ b/activerecord/lib/active_record/relation/finder_methods.rb @@ -129,9 +129,9 @@ module ActiveRecord # def first(limit = nil) if limit - find_nth_with_limit(offset_value, limit) + find_nth_with_limit(offset_index, limit) else - find_nth(:first, offset_value) + find_nth(0, offset_index) end end @@ -181,7 +181,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(:second, offset_value ? offset_value + 1 : 1) + find_nth(1, offset_index) end # Same as +second+ but raises <tt>ActiveRecord::RecordNotFound</tt> if no record @@ -197,7 +197,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(:third, offset_value ? offset_value + 2 : 2) + find_nth(2, offset_index) end # Same as +third+ but raises <tt>ActiveRecord::RecordNotFound</tt> if no record @@ -213,7 +213,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(:fourth, offset_value ? offset_value + 3 : 3) + find_nth(3, offset_index) end # Same as +fourth+ but raises <tt>ActiveRecord::RecordNotFound</tt> if no record @@ -229,7 +229,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(:fifth, offset_value ? offset_value + 4 : 4) + find_nth(4, offset_index) end # Same as +fifth+ but raises <tt>ActiveRecord::RecordNotFound</tt> if no record @@ -242,10 +242,10 @@ module ActiveRecord # If no order is defined it will order by primary key. # # Person.forty_two # returns the forty-second object fetched by SELECT * FROM people - # Person.offset(3).forty_two # returns the fifth object from OFFSET 3 (which is OFFSET 44) + # 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(:forty_two, offset_value ? offset_value + 41 : 41) + find_nth(41, offset_index) end # Same as +forty_two+ but raises <tt>ActiveRecord::RecordNotFound</tt> if no record @@ -299,11 +299,8 @@ module ActiveRecord when Array, Hash relation = relation.where(conditions) else - if conditions != :none - column = columns_hash[primary_key] - substitute = connection.substitute_at(column, bind_values.length) - relation = where(table[primary_key].eq(substitute)) - relation.bind_values += [[column, conditions]] + unless conditions == :none + relation = where(primary_key => conditions) end end @@ -334,6 +331,10 @@ module ActiveRecord private + def offset_index + offset_value || 0 + end + def find_with_associations join_dependency = construct_join_dependency @@ -347,7 +348,8 @@ module ActiveRecord if ActiveRecord::NullRelation === relation [] else - rows = connection.select_all(relation.arel, 'SQL', relation.bind_values.dup) + arel = relation.arel + rows = connection.select_all(arel, 'SQL', arel.bind_values + relation.bind_values) join_dependency.instantiate(rows, aliases) end end @@ -468,20 +470,24 @@ module ActiveRecord end end - def find_nth(ordinal, offset) + def find_nth(index, offset) if loaded? - @records.send(ordinal) + @records[index] else + offset += index @offsets[offset] ||= find_nth_with_limit(offset, 1).first end end def find_nth_with_limit(offset, limit) - if order_values.empty? && primary_key - order(arel_table[primary_key].asc).limit(limit).offset(offset).to_a - else - limit(limit).offset(offset).to_a - end + relation = if order_values.empty? && primary_key + order(arel_table[primary_key].asc) + else + self + end + + relation = relation.offset(offset) unless offset.zero? + relation.limit(limit).to_a end def find_last diff --git a/activerecord/lib/active_record/relation/merger.rb b/activerecord/lib/active_record/relation/merger.rb index 182b9ed89c..ac41d0aa80 100644 --- a/activerecord/lib/active_record/relation/merger.rb +++ b/activerecord/lib/active_record/relation/merger.rb @@ -30,6 +30,8 @@ module ActiveRecord else other.joins!(*v) end + elsif k == :select + other._select!(v) else other.send("#{k}!", v) end @@ -62,7 +64,13 @@ module ActiveRecord # expensive), most of the time the value is going to be `nil` or `.blank?`, the only catch is that # `false.blank?` returns `true`, so there needs to be an extra check so that explicit `false` values # don't fall through the cracks. - relation.send("#{name}!", *value) unless value.nil? || (value.blank? && false != value) + unless value.nil? || (value.blank? && false != value) + if name == :select + relation._select!(*value) + else + relation.send("#{name}!", *value) + end + end end merge_multi_values @@ -139,7 +147,6 @@ module ActiveRecord def merge_single_values relation.from_value = values[:from] unless relation.from_value relation.lock_value = values[:lock] unless relation.lock_value - relation.reverse_order_value = values[:reverse_order] unless values[:create_with].blank? relation.create_with_value = (relation.create_with_value || {}).merge(values[:create_with]) @@ -149,7 +156,7 @@ module ActiveRecord def filter_binds(lhs_binds, removed_wheres) return lhs_binds if removed_wheres.empty? - set = Set.new removed_wheres.map { |x| x.left.name } + set = Set.new removed_wheres.map { |x| x.left.name.to_s } lhs_binds.dup.delete_if { |col,_| set.include? col.name } end diff --git a/activerecord/lib/active_record/relation/predicate_builder.rb b/activerecord/lib/active_record/relation/predicate_builder.rb index 1252af7635..d40f276968 100644 --- a/activerecord/lib/active_record/relation/predicate_builder.rb +++ b/activerecord/lib/active_record/relation/predicate_builder.rb @@ -113,13 +113,14 @@ module ActiveRecord register_handler(Relation, RelationHandler.new) register_handler(Array, ArrayHandler.new) - private - def self.build(attribute, value) - handler_for(value).call(attribute, value) - end + def self.build(attribute, value) + handler_for(value).call(attribute, value) + end + private_class_method :build - def self.handler_for(object) - @handlers.detect { |klass, _| klass === object }.last - end + def self.handler_for(object) + @handlers.detect { |klass, _| klass === object }.last + end + private_class_method :handler_for end end diff --git a/activerecord/lib/active_record/relation/query_methods.rb b/activerecord/lib/active_record/relation/query_methods.rb index e41df0ea29..416f2305d2 100644 --- a/activerecord/lib/active_record/relation/query_methods.rb +++ b/activerecord/lib/active_record/relation/query_methods.rb @@ -64,6 +64,7 @@ module ActiveRecord # def #{name}_values=(values) # def select_values=(values) raise ImmutableRelation if @loaded # raise ImmutableRelation if @loaded + check_cached_relation @values[:#{name}] = values # @values[:select] = values end # end CODE @@ -81,11 +82,22 @@ module ActiveRecord class_eval <<-CODE, __FILE__, __LINE__ + 1 def #{name}_value=(value) # def readonly_value=(value) raise ImmutableRelation if @loaded # raise ImmutableRelation if @loaded + check_cached_relation @values[:#{name}] = value # @values[:readonly] = value end # end CODE end + def check_cached_relation # :nodoc: + if defined?(@arel) && @arel + @arel = nil + ActiveSupport::Deprecation.warn <<-WARNING +Modifying already cached Relation. The cache will be reset. +Use a cloned Relation to prevent this warning. +WARNING + end + end + def create_with_value # :nodoc: @values[:create_with] || {} end @@ -170,7 +182,7 @@ module ActiveRecord # Use to indicate that the given +table_names+ are referenced by an SQL string, # and should therefore be JOINed in any query rather than loaded separately. - # This method only works in conjuction with +includes+. + # This method only works in conjunction with +includes+. # See #includes for more details. # # User.includes(:posts).where("posts.name = 'foo'") @@ -235,11 +247,11 @@ module ActiveRecord to_a.select { |*block_args| yield(*block_args) } else raise ArgumentError, 'Call this with at least one field' if fields.empty? - spawn.select!(*fields) + spawn._select!(*fields) end end - def select!(*fields) # :nodoc: + def _select!(*fields) # :nodoc: fields.flatten! fields.map! do |field| klass.attribute_alias?(field) ? klass.attribute_alias(field) : field @@ -263,6 +275,10 @@ module ActiveRecord # # User.group('name AS grouped_name, age') # => [#<User id: 3, name: "Foo", age: 21, ...>, #<User id: 2, name: "Oscar", age: 21, ...>, #<User id: 5, name: "Foo", age: 23, ...>] + # + # Passing in an array of attributes to group by is also supported. + # User.select([:id, :first_name]).group(:id, :first_name).first(3) + # => [#<User id: 1, first_name: "Bill">, #<User id: 2, first_name: "Earl">, #<User id: 3, first_name: "Beto">] def group(*args) check_if_method_has_arguments!(:group, args) spawn.group!(*args) @@ -821,7 +837,9 @@ module ActiveRecord end def reverse_order! # :nodoc: - self.reverse_order_value = !reverse_order_value + orders = order_values.uniq + orders.reject!(&:blank?) + self.order_values = reverse_sql_order(orders) self end @@ -837,7 +855,7 @@ module ActiveRecord build_joins(arel, joins_values.flatten) unless joins_values.empty? - collapse_wheres(arel, (where_values - ['']).uniq) + collapse_wheres(arel, (where_values - [''])) #TODO: Add uniq with real value comparison / ignore uniqs that have binds arel.having(*having_values.uniq.reject(&:blank?)) unless having_values.empty? @@ -854,6 +872,15 @@ module ActiveRecord arel.from(build_from) if from_value arel.lock(lock_value) if lock_value + # Reorder bind indexes if joins produced bind values + if arel.bind_values.any? + bvs = arel.bind_values + bind_values + arel.ast.grep(Arel::Nodes::BindParam).each_with_index do |bp, i| + column = bvs[i].first + bp.replace connection.substitute_at(column, i) + end + end + arel end @@ -867,8 +894,9 @@ module ActiveRecord case scope when :order - self.reverse_order_value = false result = [] + when :where + self.bind_values = [] else result = [] unless single_val_method end @@ -919,18 +947,16 @@ module ActiveRecord def build_where(opts, other = []) case opts when String, Array - #TODO: Remove duplication with: /activerecord/lib/active_record/sanitization.rb:113 - values = Hash === other.first ? other.first.values : other - - values.grep(ActiveRecord::Relation) do |rel| - self.bind_values += rel.bind_values - end - [@klass.send(:sanitize_sql, other.empty? ? opts : ([opts] + other))] when Hash opts = PredicateBuilder.resolve_column_aliases(klass, opts) attributes = @klass.send(:expand_hash_conditions_for_aggregates, opts) + bv_len = bind_values.length + tmp_opts, bind_values = create_binds(opts, bv_len) + self.bind_values += bind_values + + attributes = @klass.send(:expand_hash_conditions_for_aggregates, tmp_opts) attributes.values.grep(ActiveRecord::Relation) do |rel| self.bind_values += rel.bind_values end @@ -941,6 +967,29 @@ module ActiveRecord end end + def create_binds(opts, idx) + bindable, non_binds = opts.partition do |column, value| + case value + when String, Integer, ActiveRecord::StatementCache::Substitute + @klass.columns_hash.include? column.to_s + else + false + end + end + + new_opts = {} + binds = [] + + bindable.each_with_index do |(column,value), index| + binds.push [@klass.columns_hash[column.to_s], value] + new_opts[column] = connection.substitute_at(column, index + idx) + end + + non_binds.each { |column,value| new_opts[column] = value } + + [new_opts, binds] + end + def build_from opts, name = from_value case opts @@ -982,9 +1031,12 @@ module ActiveRecord join_list ) - joins = join_dependency.join_constraints stashed_association_joins + join_infos = join_dependency.join_constraints stashed_association_joins - joins.each { |join| manager.from(join) } + join_infos.each do |info| + info.joins.each { |join| manager.from(join) } + manager.bind_values.concat info.binds + end manager.join_sources.concat(join_list) @@ -1027,7 +1079,6 @@ module ActiveRecord def build_order(arel) orders = order_values.uniq orders.reject!(&:blank?) - orders = reverse_sql_order(orders) if reverse_order_value arel.order(*orders) unless orders.empty? end diff --git a/activerecord/lib/active_record/relation/spawn_methods.rb b/activerecord/lib/active_record/relation/spawn_methods.rb index 2552cbd234..57d66bce4b 100644 --- a/activerecord/lib/active_record/relation/spawn_methods.rb +++ b/activerecord/lib/active_record/relation/spawn_methods.rb @@ -58,6 +58,9 @@ module ActiveRecord # Post.order('id asc').only(:where) # discards the order condition # Post.order('id asc').only(:where, :order) # uses the specified order def only(*onlies) + if onlies.any? { |o| o == :where } + onlies << :bind + end relation_with values.slice(*onlies) end |