aboutsummaryrefslogtreecommitdiffstats
path: root/activerecord/lib/active_record/relation
diff options
context:
space:
mode:
Diffstat (limited to 'activerecord/lib/active_record/relation')
-rw-r--r--activerecord/lib/active_record/relation/batches.rb131
-rw-r--r--activerecord/lib/active_record/relation/calculations.rb312
-rw-r--r--activerecord/lib/active_record/relation/delegation.rb55
-rw-r--r--activerecord/lib/active_record/relation/finder_methods.rb389
-rw-r--r--activerecord/lib/active_record/relation/merger.rb120
-rw-r--r--activerecord/lib/active_record/relation/predicate_builder.rb179
-rw-r--r--activerecord/lib/active_record/relation/predicate_builder/array_handler.rb10
-rw-r--r--activerecord/lib/active_record/relation/predicate_builder/association_query_handler.rb42
-rw-r--r--activerecord/lib/active_record/relation/predicate_builder/base_handler.rb2
-rw-r--r--activerecord/lib/active_record/relation/predicate_builder/basic_object_handler.rb8
-rw-r--r--activerecord/lib/active_record/relation/predicate_builder/class_handler.rb14
-rw-r--r--activerecord/lib/active_record/relation/predicate_builder/polymorphic_array_handler.rb20
-rw-r--r--activerecord/lib/active_record/relation/predicate_builder/range_handler.rb8
-rw-r--r--activerecord/lib/active_record/relation/query_attribute.rb2
-rw-r--r--activerecord/lib/active_record/relation/query_methods.rb527
-rw-r--r--activerecord/lib/active_record/relation/record_fetch_warning.rb6
-rw-r--r--activerecord/lib/active_record/relation/spawn_methods.rb7
-rw-r--r--activerecord/lib/active_record/relation/where_clause.rb122
-rw-r--r--activerecord/lib/active_record/relation/where_clause_factory.rb7
19 files changed, 968 insertions, 993 deletions
diff --git a/activerecord/lib/active_record/relation/batches.rb b/activerecord/lib/active_record/relation/batches.rb
index 3639625722..4b2987ac6d 100644
--- a/activerecord/lib/active_record/relation/batches.rb
+++ b/activerecord/lib/active_record/relation/batches.rb
@@ -2,7 +2,7 @@ require "active_record/relation/batches/batch_enumerator"
module ActiveRecord
module Batches
- ORDER_OR_LIMIT_IGNORED_MESSAGE = "Scoped order and limit are ignored, it's forced to be batch order and batch size."
+ ORDER_IGNORE_MESSAGE = "Scoped order is ignored, it's forced to be batch order."
# Looping through a collection of records from the database
# (using the Scoping::Named::ClassMethods.all method, for example)
@@ -34,15 +34,19 @@ module ActiveRecord
# * <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.
# * <tt>:error_on_ignore</tt> - Overrides the application config to specify if an error should be raised when
- # the order and limit have to be ignored due to batching.
+ # an order is present in the relation.
#
- # 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 +:start+ and +:finish+ option on each worker).
+ # Limits are honored, and if present there is no requirement for the batch
+ # size, it can be less than, equal, or greater than the limit.
#
- # # Let's process for a batch of 2000 records, skipping the first 2000 rows
- # Person.find_each(start: 2000, batch_size: 2000) do |person|
+ # The options +start+ and +finish+ are especially useful if you want
+ # multiple workers dealing with the same processing queue. You can make
+ # worker 1 handle all the records between id 1 and 9999 and worker 2
+ # handle from 10000 and beyond by setting the +:start+ and +:finish+
+ # option on each worker.
+ #
+ # # Let's process from record 10_000 on.
+ # Person.find_each(start: 10_000) do |person|
# person.party_all_night!
# end
#
@@ -51,8 +55,8 @@ module ActiveRecord
# work. This also means that this method only works when the primary key is
# orderable (e.g. an integer or string).
#
- # NOTE: You can't set the limit either, that's used to control
- # the batch sizes.
+ # NOTE: By its nature, batch processing is subject to race conditions if
+ # other processes are modifying the database.
def find_each(start: nil, finish: nil, batch_size: 1000, error_on_ignore: nil)
if block_given?
find_in_batches(start: start, finish: finish, batch_size: batch_size, error_on_ignore: error_on_ignore) do |records|
@@ -89,15 +93,19 @@ module ActiveRecord
# * <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.
# * <tt>:error_on_ignore</tt> - Overrides the application config to specify if an error should be raised when
- # the order and limit have to be ignored due to batching.
+ # an order is present in the relation.
+ #
+ # Limits are honored, and if present there is no requirement for the batch
+ # size, it can be less than, equal, or greater than the limit.
#
- # 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 +:start+ and +:finish+ option on each worker).
+ # The options +start+ and +finish+ are especially useful if you want
+ # multiple workers dealing with the same processing queue. You can make
+ # worker 1 handle all the records between id 1 and 9999 and worker 2
+ # handle from 10000 and beyond by setting the +:start+ and +:finish+
+ # option on each worker.
#
- # # Let's process the next 2000 records
- # Person.find_in_batches(start: 2000, batch_size: 2000) do |group|
+ # # Let's process from record 10_000 on.
+ # Person.find_in_batches(start: 10_000) do |group|
# group.each { |person| person.party_all_night! }
# end
#
@@ -106,8 +114,8 @@ module ActiveRecord
# work. This also means that this method only works when the primary key is
# orderable (e.g. an integer or string).
#
- # NOTE: You can't set the limit either, that's used to control
- # the batch sizes.
+ # NOTE: By its nature, batch processing is subject to race conditions if
+ # other processes are modifying the database.
def find_in_batches(start: nil, finish: nil, batch_size: 1000, error_on_ignore: nil)
relation = self
unless block_given?
@@ -149,17 +157,19 @@ module ActiveRecord
# * <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.
# * <tt>:error_on_ignore</tt> - Overrides the application config to specify if an error should be raised when
- # the order and limit have to be ignored due to batching.
+ # an order is present in the relation.
#
- # 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 +:start+ and +:finish+
- # option on each worker).
+ # Limits are honored, and if present there is no requirement for the batch
+ # size, it can be less than, equal, or greater than the limit.
#
- # # Let's process the next 2000 records
- # Person.in_batches(of: 2000, start: 2000).update_all(awesome: true)
+ # The options +start+ and +finish+ are especially useful if you want
+ # multiple workers dealing with the same processing queue. You can make
+ # worker 1 handle all the records between id 1 and 9999 and worker 2
+ # handle from 10000 and beyond by setting the +:start+ and +:finish+
+ # option on each worker.
+ #
+ # # Let's process from record 10_000 on.
+ # Person.in_batches(start: 10_000).update_all(awesome: true)
#
# An example of calling where query method on the relation:
#
@@ -179,19 +189,25 @@ module ActiveRecord
# consistent. Therefore the primary key must be orderable, e.g an integer
# or a string.
#
- # NOTE: You can't set the limit either, that's used to control the batch
- # sizes.
+ # NOTE: By its nature, batch processing is subject to race conditions if
+ # other processes are modifying the database.
def in_batches(of: 1000, start: nil, finish: nil, load: false, error_on_ignore: nil)
relation = self
unless block_given?
return BatchEnumerator.new(of: of, start: start, finish: finish, relation: self)
end
- if arel.orders.present? || arel.taken.present?
- act_on_order_or_limit_ignored(error_on_ignore)
+ if arel.orders.present?
+ act_on_ignored_order(error_on_ignore)
+ end
+
+ batch_limit = of
+ if limit_value
+ remaining = limit_value
+ batch_limit = remaining if remaining < batch_limit
end
- relation = relation.reorder(batch_order).limit(of)
+ relation = relation.reorder(batch_order).limit(batch_limit)
relation = apply_limits(relation, start, finish)
batch_relation = relation
@@ -199,11 +215,11 @@ module ActiveRecord
if load
records = batch_relation.records
ids = records.map(&:id)
- yielded_relation = self.where(primary_key => ids)
+ yielded_relation = where(primary_key => ids)
yielded_relation.load_records(records)
else
ids = batch_relation.pluck(primary_key)
- yielded_relation = self.where(primary_key => ids)
+ yielded_relation = where(primary_key => ids)
end
break if ids.empty?
@@ -213,31 +229,44 @@ module ActiveRecord
yield yielded_relation
- break if ids.length < of
+ break if ids.length < batch_limit
+
+ if limit_value
+ remaining -= ids.length
+
+ if remaining == 0
+ # Saves a useless iteration when the limit is a multiple of the
+ # batch size.
+ break
+ elsif remaining < batch_limit
+ relation = relation.limit(remaining)
+ end
+ end
+
batch_relation = relation.where(arel_attribute(primary_key).gt(primary_key_offset))
end
end
private
- def apply_limits(relation, start, 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
+ def apply_limits(relation, start, 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
- def batch_order
- "#{quoted_table_name}.#{quoted_primary_key} ASC"
- end
+ def batch_order
+ "#{quoted_table_name}.#{quoted_primary_key} ASC"
+ end
- def act_on_order_or_limit_ignored(error_on_ignore)
- raise_error = (error_on_ignore.nil? ? self.klass.error_on_ignored_order_or_limit : error_on_ignore)
+ def act_on_ignored_order(error_on_ignore)
+ raise_error = (error_on_ignore.nil? ? self.klass.error_on_ignored_order : error_on_ignore)
- if raise_error
- raise ArgumentError.new(ORDER_OR_LIMIT_IGNORED_MESSAGE)
- elsif logger
- logger.warn(ORDER_OR_LIMIT_IGNORED_MESSAGE)
+ if raise_error
+ raise ArgumentError.new(ORDER_IGNORE_MESSAGE)
+ elsif logger
+ logger.warn(ORDER_IGNORE_MESSAGE)
+ end
end
- end
end
end
diff --git a/activerecord/lib/active_record/relation/calculations.rb b/activerecord/lib/active_record/relation/calculations.rb
index d6d92b8607..e4676f79a5 100644
--- a/activerecord/lib/active_record/relation/calculations.rb
+++ b/activerecord/lib/active_record/relation/calculations.rb
@@ -112,12 +112,11 @@ module ActiveRecord
# ...
# end
def calculate(operation, column_name)
- if column_name.is_a?(Symbol) && attribute_alias?(column_name)
- column_name = attribute_alias(column_name)
- end
-
if has_include?(column_name)
- construct_relation_for_association_calculations.calculate(operation, column_name)
+ relation = construct_relation_for_association_calculations
+ relation = relation.distinct if operation.to_s.downcase == "count"
+
+ relation.calculate(operation, column_name)
else
perform_calculation(operation, column_name)
end
@@ -160,7 +159,7 @@ module ActiveRecord
#
def pluck(*column_names)
if loaded? && (column_names.map(&:to_s) - @klass.attribute_names - @klass.attribute_aliases.keys).empty?
- return @records.pluck(*column_names)
+ return records.pluck(*column_names)
end
if has_include?(column_names.first)
@@ -185,201 +184,196 @@ module ActiveRecord
private
- def has_include?(column_name)
- eager_loading? || (includes_values.present? && column_name && column_name != :all)
- end
-
- def perform_calculation(operation, column_name)
- operation = operation.to_s.downcase
+ def has_include?(column_name)
+ eager_loading? || (includes_values.present? && column_name && column_name != :all)
+ end
- # If #count is used with #distinct (i.e. `relation.distinct.count`) it is
- # considered distinct.
- distinct = self.distinct_value
+ def perform_calculation(operation, column_name)
+ operation = operation.to_s.downcase
- if operation == "count"
- column_name ||= select_for_count
+ # If #count is used with #distinct (i.e. `relation.distinct.count`) it is
+ # considered distinct.
+ distinct = self.distinct_value
- unless arel.ast.grep(Arel::Nodes::OuterJoin).empty?
- distinct = true
+ if operation == "count"
+ column_name ||= select_for_count
+ column_name = primary_key if column_name == :all && distinct
+ distinct = nil if column_name =~ /\s*DISTINCT[\s(]+/i
end
- column_name = primary_key if column_name == :all && distinct
- distinct = nil if column_name =~ /\s*DISTINCT[\s(]+/i
- end
-
- if group_values.any?
- execute_grouped_calculation(operation, column_name, distinct)
- else
- execute_simple_calculation(operation, column_name, distinct)
+ if group_values.any?
+ execute_grouped_calculation(operation, column_name, distinct)
+ else
+ execute_simple_calculation(operation, column_name, distinct)
+ end
end
- end
- def aggregate_column(column_name)
- return column_name if Arel::Expressions === column_name
+ def aggregate_column(column_name)
+ return column_name if Arel::Expressions === column_name
- if @klass.column_names.include?(column_name.to_s)
- Arel::Attribute.new(@klass.unscoped.table, column_name)
- else
- Arel.sql(column_name == :all ? "*" : column_name.to_s)
+ if @klass.has_attribute?(column_name.to_s) || @klass.attribute_alias?(column_name.to_s)
+ @klass.arel_attribute(column_name)
+ else
+ Arel.sql(column_name == :all ? "*" : column_name.to_s)
+ end
end
- end
-
- def operation_over_aggregate_column(column, operation, distinct)
- operation == 'count' ? column.count(distinct) : column.send(operation)
- end
- def execute_simple_calculation(operation, column_name, distinct) #:nodoc:
- # PostgreSQL doesn't like ORDER BY when there are no GROUP BY
- relation = unscope(:order)
-
- column_alias = column_name
+ def operation_over_aggregate_column(column, operation, distinct)
+ operation == "count" ? column.count(distinct) : column.send(operation)
+ end
- if operation == "count" && (relation.limit_value || relation.offset_value)
- # Shortcut when limit is zero.
- return 0 if relation.limit_value == 0
+ def execute_simple_calculation(operation, column_name, distinct) #:nodoc:
+ # PostgreSQL doesn't like ORDER BY when there are no GROUP BY
+ relation = unscope(:order)
- query_builder = build_count_subquery(relation, column_name, distinct)
- else
- column = aggregate_column(column_name)
+ column_alias = column_name
- select_value = operation_over_aggregate_column(column, operation, distinct)
+ if operation == "count" && (relation.limit_value || relation.offset_value)
+ # Shortcut when limit is zero.
+ return 0 if relation.limit_value == 0
- column_alias = select_value.alias
- column_alias ||= @klass.connection.column_name_for_operation(operation, select_value)
- relation.select_values = [select_value]
+ query_builder = build_count_subquery(relation, column_name, distinct)
+ else
+ column = aggregate_column(column_name)
- query_builder = relation.arel
- end
+ select_value = operation_over_aggregate_column(column, operation, distinct)
- result = @klass.connection.select_all(query_builder, nil, bound_attributes)
- row = result.first
- value = row && row.values.first
- column = result.column_types.fetch(column_alias) do
- type_for(column_name)
- end
+ column_alias = select_value.alias
+ column_alias ||= @klass.connection.column_name_for_operation(operation, select_value)
+ relation.select_values = [select_value]
- type_cast_calculated_value(value, column, operation)
- end
+ query_builder = relation.arel
+ end
- def execute_grouped_calculation(operation, column_name, distinct) #:nodoc:
- group_attrs = group_values
+ result = @klass.connection.select_all(query_builder, nil, bound_attributes)
+ row = result.first
+ value = row && row.values.first
+ type = result.column_types.fetch(column_alias) do
+ type_for(column_name)
+ end
- if group_attrs.first.respond_to?(:to_sym)
- association = @klass._reflect_on_association(group_attrs.first)
- associated = group_attrs.size == 1 && association && association.belongs_to? # only count belongs_to associations
- group_fields = Array(associated ? association.foreign_key : group_attrs)
- else
- group_fields = group_attrs
+ type_cast_calculated_value(value, type, operation)
end
- group_fields = arel_columns(group_fields)
-
- group_aliases = group_fields.map { |field| column_alias_for(field) }
- group_columns = group_aliases.zip(group_fields)
- if operation == 'count' && column_name == :all
- aggregate_alias = 'count_all'
- else
- aggregate_alias = column_alias_for([operation, column_name].join(' '))
- end
+ def execute_grouped_calculation(operation, column_name, distinct) #:nodoc:
+ group_attrs = group_values
- select_values = [
- operation_over_aggregate_column(
- aggregate_column(column_name),
- operation,
- distinct).as(aggregate_alias)
- ]
- select_values += select_values unless having_clause.empty?
-
- select_values.concat group_columns.map { |aliaz, field|
- if field.respond_to?(:as)
- field.as(aliaz)
+ if group_attrs.first.respond_to?(:to_sym)
+ association = @klass._reflect_on_association(group_attrs.first)
+ associated = group_attrs.size == 1 && association && association.belongs_to? # only count belongs_to associations
+ group_fields = Array(associated ? association.foreign_key : group_attrs)
else
- "#{field} AS #{aliaz}"
+ group_fields = group_attrs
end
- }
+ group_fields = arel_columns(group_fields)
- relation = except(:group)
- relation.group_values = group_fields
- relation.select_values = select_values
+ group_aliases = group_fields.map { |field| column_alias_for(field) }
+ group_columns = group_aliases.zip(group_fields)
- calculated_data = @klass.connection.select_all(relation, nil, relation.bound_attributes)
-
- if association
- key_ids = calculated_data.collect { |row| row[group_aliases.first] }
- key_records = association.klass.base_class.where(association.klass.base_class.primary_key => key_ids)
- key_records = Hash[key_records.map { |r| [r.id, r] }]
- end
+ if operation == "count" && column_name == :all
+ aggregate_alias = "count_all"
+ else
+ aggregate_alias = column_alias_for([operation, column_name].join(" "))
+ end
- Hash[calculated_data.map do |row|
- key = group_columns.map { |aliaz, col_name|
- column = calculated_data.column_types.fetch(aliaz) do
- type_for(col_name)
+ select_values = [
+ operation_over_aggregate_column(
+ aggregate_column(column_name),
+ operation,
+ distinct).as(aggregate_alias)
+ ]
+ select_values += select_values unless having_clause.empty?
+
+ select_values.concat group_columns.map { |aliaz, field|
+ if field.respond_to?(:as)
+ field.as(aliaz)
+ else
+ "#{field} AS #{aliaz}"
end
- type_cast_calculated_value(row[aliaz], column)
}
- key = key.first if key.size == 1
- key = key_records[key] if associated
- column_type = calculated_data.column_types.fetch(aggregate_alias) { type_for(column_name) }
- [key, type_cast_calculated_value(row[aggregate_alias], column_type, operation)]
- end]
- end
+ relation = except(:group)
+ relation.group_values = group_fields
+ relation.select_values = select_values
- # Converts the given keys to the value that the database adapter returns as
- # a usable column name:
- #
- # column_alias_for("users.id") # => "users_id"
- # column_alias_for("sum(id)") # => "sum_id"
- # column_alias_for("count(distinct users.id)") # => "count_distinct_users_id"
- # column_alias_for("count(*)") # => "count_all"
- def column_alias_for(keys)
- if keys.respond_to? :name
- keys = "#{keys.relation.name}.#{keys.name}"
+ calculated_data = @klass.connection.select_all(relation, nil, relation.bound_attributes)
+
+ if association
+ key_ids = calculated_data.collect { |row| row[group_aliases.first] }
+ key_records = association.klass.base_class.where(association.klass.base_class.primary_key => key_ids)
+ key_records = Hash[key_records.map { |r| [r.id, r] }]
+ end
+
+ Hash[calculated_data.map do |row|
+ key = group_columns.map { |aliaz, col_name|
+ type = type_for(col_name) do
+ calculated_data.column_types.fetch(aliaz, Type.default_value)
+ end
+ type_cast_calculated_value(row[aliaz], type)
+ }
+ key = key.first if key.size == 1
+ key = key_records[key] if associated
+
+ type = calculated_data.column_types.fetch(aggregate_alias) { type_for(column_name) }
+ [key, type_cast_calculated_value(row[aggregate_alias], type, operation)]
+ end]
end
- table_name = keys.to_s.downcase
- table_name.gsub!(/\*/, 'all')
- table_name.gsub!(/\W+/, ' ')
- table_name.strip!
- table_name.gsub!(/ +/, '_')
+ # Converts the given keys to the value that the database adapter returns as
+ # a usable column name:
+ #
+ # column_alias_for("users.id") # => "users_id"
+ # column_alias_for("sum(id)") # => "sum_id"
+ # column_alias_for("count(distinct users.id)") # => "count_distinct_users_id"
+ # column_alias_for("count(*)") # => "count_all"
+ def column_alias_for(keys)
+ if keys.respond_to? :name
+ keys = "#{keys.relation.name}.#{keys.name}"
+ end
- @klass.connection.table_alias_for(table_name)
- end
+ table_name = keys.to_s.downcase
+ table_name.gsub!(/\*/, "all")
+ table_name.gsub!(/\W+/, " ")
+ table_name.strip!
+ table_name.gsub!(/ +/, "_")
- def type_for(field)
- field_name = field.respond_to?(:name) ? field.name.to_s : field.to_s.split('.').last
- @klass.type_for_attribute(field_name)
- end
+ @klass.connection.table_alias_for(table_name)
+ end
- def type_cast_calculated_value(value, type, operation = nil)
- case operation
- when 'count' then value.to_i
- when 'sum' then type.deserialize(value || 0)
- when 'average' then value.respond_to?(:to_d) ? value.to_d : value
+ def type_for(field, &block)
+ field_name = field.respond_to?(:name) ? field.name.to_s : field.to_s.split(".").last
+ @klass.type_for_attribute(field_name, &block)
+ end
+
+ def type_cast_calculated_value(value, type, operation = nil)
+ case operation
+ when "count" then value.to_i
+ when "sum" then type.deserialize(value || 0)
+ when "average" then value.respond_to?(:to_d) ? value.to_d : value
else type.deserialize(value)
+ end
end
- end
- def select_for_count
- if select_values.present?
- return select_values.first if select_values.one?
- select_values.join(", ")
- else
- :all
+ def select_for_count
+ if select_values.present?
+ return select_values.first if select_values.one?
+ select_values.join(", ")
+ else
+ :all
+ end
end
- end
- def build_count_subquery(relation, column_name, distinct)
- column_alias = Arel.sql('count_column')
- subquery_alias = Arel.sql('subquery_for_count')
+ def build_count_subquery(relation, column_name, distinct)
+ column_alias = Arel.sql("count_column")
+ subquery_alias = Arel.sql("subquery_for_count")
- aliased_column = aggregate_column(column_name == :all ? 1 : column_name).as(column_alias)
- relation.select_values = [aliased_column]
- subquery = relation.arel.as(subquery_alias)
+ aliased_column = aggregate_column(column_name == :all ? 1 : column_name).as(column_alias)
+ relation.select_values = [aliased_column]
+ subquery = relation.arel.as(subquery_alias)
- sm = Arel::SelectManager.new relation.engine
- select_value = operation_over_aggregate_column(column_alias, 'count', distinct)
- sm.project(select_value).from(subquery)
- end
+ sm = Arel::SelectManager.new relation.engine
+ select_value = operation_over_aggregate_column(column_alias, "count", distinct)
+ sm.project(select_value).from(subquery)
+ end
end
end
diff --git a/activerecord/lib/active_record/relation/delegation.rb b/activerecord/lib/active_record/relation/delegation.rb
index 2484cb3264..d16de4b06c 100644
--- a/activerecord/lib/active_record/relation/delegation.rb
+++ b/activerecord/lib/active_record/relation/delegation.rb
@@ -1,4 +1,5 @@
-require 'active_support/concern'
+require "active_support/concern"
+require "active_support/core_ext/regexp"
module ActiveRecord
module Delegation # :nodoc:
@@ -17,7 +18,7 @@ module ActiveRecord
delegate = Class.new(klass) {
include ClassSpecificRelation
}
- const_set klass.name.gsub('::'.freeze, '_'.freeze), delegate
+ const_set klass.name.gsub("::".freeze, "_".freeze), delegate
cache[klass] = delegate
end
end
@@ -37,10 +38,10 @@ module ActiveRecord
delegate :to_xml, :encode_with, :length, :collect, :map, :each, :all?, :include?, :to_ary, :join,
:[], :&, :|, :+, :-, :sample, :reverse, :compact, :in_groups, :in_groups_of,
- :shuffle, :split, to: :records
+ :shuffle, :split, :index, to: :records
delegate :table_name, :quoted_table_name, :primary_key, :quoted_primary_key,
- :connection, :columns_hash, :to => :klass
+ :connection, :columns_hash, to: :klass
module ClassSpecificRelation # :nodoc:
extend ActiveSupport::Concern
@@ -58,7 +59,7 @@ module ActiveRecord
@delegation_mutex.synchronize do
return if method_defined?(method)
- if method.to_s =~ /\A[a-zA-Z_]\w*[!?]?\z/
+ if /\A[a-zA-Z_]\w*[!?]?\z/.match?(method)
module_eval <<-RUBY, __FILE__, __LINE__ + 1
def #{method}(*args, &block)
scoping { @klass.#{method}(*args, &block) }
@@ -82,17 +83,17 @@ module ActiveRecord
protected
- def method_missing(method, *args, &block)
- if @klass.respond_to?(method)
- self.class.delegate_to_scoped_klass(method)
- scoping { @klass.public_send(method, *args, &block) }
- elsif arel.respond_to?(method)
- self.class.delegate method, :to => :arel
- arel.public_send(method, *args, &block)
- else
- super
+ def method_missing(method, *args, &block)
+ if @klass.respond_to?(method)
+ self.class.delegate_to_scoped_klass(method)
+ scoping { @klass.public_send(method, *args, &block) }
+ elsif arel.respond_to?(method)
+ self.class.delegate method, to: :arel
+ arel.public_send(method, *args, &block)
+ else
+ super
+ end
end
- end
end
module ClassMethods # :nodoc:
@@ -102,26 +103,26 @@ module ActiveRecord
private
- def relation_class_for(klass)
- klass.relation_delegate_class(self)
- end
+ def relation_class_for(klass)
+ klass.relation_delegate_class(self)
+ end
end
- def respond_to?(method, include_private = false)
+ def respond_to_missing?(method, include_private = false)
super || @klass.respond_to?(method, include_private) ||
arel.respond_to?(method, include_private)
end
protected
- def method_missing(method, *args, &block)
- if @klass.respond_to?(method)
- scoping { @klass.public_send(method, *args, &block) }
- elsif arel.respond_to?(method)
- arel.public_send(method, *args, &block)
- else
- super
+ def method_missing(method, *args, &block)
+ if @klass.respond_to?(method)
+ scoping { @klass.public_send(method, *args, &block) }
+ elsif arel.respond_to?(method)
+ arel.public_send(method, *args, &block)
+ else
+ super
+ end
end
- end
end
end
diff --git a/activerecord/lib/active_record/relation/finder_methods.rb b/activerecord/lib/active_record/relation/finder_methods.rb
index d255cad91b..97a819c5af 100644
--- a/activerecord/lib/active_record/relation/finder_methods.rb
+++ b/activerecord/lib/active_record/relation/finder_methods.rb
@@ -1,8 +1,8 @@
-require 'active_support/core_ext/string/filters'
+require "active_support/core_ext/string/filters"
module ActiveRecord
module FinderMethods
- ONE_AS_ONE = '1 AS one'
+ ONE_AS_ONE = "1 AS one"
# Find by id - This can either be a specific id (1), a list of ids (1, 5, 6), or an array of ids ([5, 6, 10]).
# If one or more records can not be found for the requested ids, then RecordNotFound will be raised. If the primary key
@@ -97,13 +97,13 @@ module ActiveRecord
# Person.take(5) # returns 5 objects fetched by SELECT * FROM people LIMIT 5
# Person.where(["name LIKE '%?'", name]).take
def take(limit = nil)
- limit ? limit(limit).to_a : find_take
+ limit ? find_take_with_limit(limit) : find_take
end
# Same as #take but raises ActiveRecord::RecordNotFound if no record
# is found. Note that #take! accepts no arguments.
def take!
- take or raise RecordNotFound.new("Couldn't find #{@klass.name} with [#{arel.where_sql(@klass.arel_engine)}]")
+ take || raise_record_not_found_exception!
end
# Find the first record (or first N records if a parameter is supplied).
@@ -117,7 +117,7 @@ module ActiveRecord
#
def first(limit = nil)
if limit
- find_nth_with_limit_and_offset(0, limit, offset: offset_index)
+ find_nth_with_limit(0, limit)
else
find_nth 0
end
@@ -126,7 +126,7 @@ module ActiveRecord
# Same as #first but raises ActiveRecord::RecordNotFound if no record
# is found. Note that #first! accepts no arguments.
def first!
- find_nth! 0
+ first || raise_record_not_found_exception!
end
# Find the last record (or last N records if a parameter is supplied).
@@ -165,7 +165,7 @@ module ActiveRecord
# Same as #last but raises ActiveRecord::RecordNotFound if no record
# is found. Note that #last! accepts no arguments.
def last!
- last or raise RecordNotFound.new("Couldn't find #{@klass.name} with [#{arel.where_sql(@klass.arel_engine)}]")
+ last || raise_record_not_found_exception!
end
# Find the second record.
@@ -181,7 +181,7 @@ module ActiveRecord
# Same as #second but raises ActiveRecord::RecordNotFound if no record
# is found.
def second!
- find_nth! 1
+ second || raise_record_not_found_exception!
end
# Find the third record.
@@ -197,7 +197,7 @@ module ActiveRecord
# Same as #third but raises ActiveRecord::RecordNotFound if no record
# is found.
def third!
- find_nth! 2
+ third || raise_record_not_found_exception!
end
# Find the fourth record.
@@ -213,7 +213,7 @@ module ActiveRecord
# Same as #fourth but raises ActiveRecord::RecordNotFound if no record
# is found.
def fourth!
- find_nth! 3
+ fourth || raise_record_not_found_exception!
end
# Find the fifth record.
@@ -229,7 +229,7 @@ module ActiveRecord
# Same as #fifth but raises ActiveRecord::RecordNotFound if no record
# is found.
def fifth!
- find_nth! 4
+ fifth || raise_record_not_found_exception!
end
# Find the forty-second record. Also known as accessing "the reddit".
@@ -245,7 +245,7 @@ module ActiveRecord
# Same as #forty_two but raises ActiveRecord::RecordNotFound if no record
# is found.
def forty_two!
- find_nth! 41
+ forty_two || raise_record_not_found_exception!
end
# Find the third-to-last record.
@@ -261,7 +261,7 @@ module ActiveRecord
# 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)}]")
+ third_to_last || raise_record_not_found_exception!
end
# Find the second-to-last record.
@@ -277,7 +277,7 @@ module ActiveRecord
# 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)}]")
+ second_to_last || raise_record_not_found_exception!
end
# Returns true if a record exists in the table that matches the +id+ or
@@ -345,248 +345,237 @@ module ActiveRecord
# of results obtained should be provided in the +result_size+ argument and
# the expected number of results should be provided in the +expected_size+
# argument.
- def raise_record_not_found_exception!(ids, result_size, expected_size) #:nodoc:
+ def raise_record_not_found_exception!(ids = nil, result_size = nil, expected_size = nil) # :nodoc:
conditions = arel.where_sql(@klass.arel_engine)
conditions = " [#{conditions}]" if conditions
-
- if Array(ids).size == 1
- error = "Couldn't find #{@klass.name} with '#{primary_key}'=#{ids}#{conditions}"
+ name = @klass.name
+
+ if ids.nil?
+ error = "Couldn't find #{name}"
+ error << " with#{conditions}" if conditions
+ raise RecordNotFound.new(error, name)
+ elsif Array(ids).size == 1
+ error = "Couldn't find #{name} with '#{primary_key}'=#{ids}#{conditions}"
+ raise RecordNotFound.new(error, name, primary_key, ids)
else
- error = "Couldn't find all #{@klass.name.pluralize} with '#{primary_key}': "
+ error = "Couldn't find all #{name.pluralize} with '#{primary_key}': "
error << "(#{ids.join(", ")})#{conditions} (found #{result_size} results, but was looking for #{expected_size})"
- end
- raise RecordNotFound, error
+ raise RecordNotFound.new(error, name, primary_key, ids)
+ end
end
private
- def offset_index
- offset_value || 0
- end
+ def offset_index
+ offset_value || 0
+ end
- def find_with_associations
- # NOTE: the JoinDependency constructed here needs to know about
- # any joins already present in `self`, so pass them in
- #
- # failing to do so means that in cases like activerecord/test/cases/associations/inner_join_association_test.rb:136
- # incorrect SQL is generated. In that case, the join dependency for
- # SpecialCategorizations is constructed without knowledge of the
- # preexisting join in joins_values to categorizations (by way of
- # the `has_many :through` for categories).
- #
- join_dependency = construct_join_dependency(joins_values)
-
- aliases = join_dependency.aliases
- relation = select aliases.columns
- relation = apply_join_dependency(relation, join_dependency)
-
- if block_given?
- yield relation
- else
- if ActiveRecord::NullRelation === relation
- []
+ def find_with_associations
+ # NOTE: the JoinDependency constructed here needs to know about
+ # any joins already present in `self`, so pass them in
+ #
+ # failing to do so means that in cases like activerecord/test/cases/associations/inner_join_association_test.rb:136
+ # incorrect SQL is generated. In that case, the join dependency for
+ # SpecialCategorizations is constructed without knowledge of the
+ # preexisting join in joins_values to categorizations (by way of
+ # the `has_many :through` for categories).
+ #
+ join_dependency = construct_join_dependency(joins_values)
+
+ aliases = join_dependency.aliases
+ relation = select aliases.columns
+ relation = apply_join_dependency(relation, join_dependency)
+
+ if block_given?
+ yield relation
else
- arel = relation.arel
- rows = connection.select_all(arel, 'SQL', relation.bound_attributes)
- join_dependency.instantiate(rows, aliases)
+ if ActiveRecord::NullRelation === relation
+ []
+ else
+ arel = relation.arel
+ rows = connection.select_all(arel, "SQL", relation.bound_attributes)
+ join_dependency.instantiate(rows, aliases)
+ end
end
end
- end
- def construct_join_dependency(joins = [], eager_loading: true)
- including = eager_load_values + includes_values
- ActiveRecord::Associations::JoinDependency.new(@klass, including, joins, eager_loading: eager_loading)
- end
+ def construct_join_dependency(joins = [], eager_loading: true)
+ including = eager_load_values + includes_values
+ ActiveRecord::Associations::JoinDependency.new(@klass, including, joins, eager_loading: eager_loading)
+ end
- def construct_relation_for_association_calculations
- apply_join_dependency(self, construct_join_dependency(joins_values))
- end
+ def construct_relation_for_association_calculations
+ apply_join_dependency(self, construct_join_dependency(joins_values))
+ end
- def apply_join_dependency(relation, join_dependency)
- relation = relation.except(:includes, :eager_load, :preload)
- relation = relation.joins join_dependency
+ def apply_join_dependency(relation, join_dependency)
+ relation = relation.except(:includes, :eager_load, :preload)
+ relation = relation.joins join_dependency
- if using_limitable_reflections?(join_dependency.reflections)
- relation
- else
- if relation.limit_value
- limited_ids = limited_ids_for(relation)
- limited_ids.empty? ? relation.none! : relation.where!(primary_key => limited_ids)
+ if using_limitable_reflections?(join_dependency.reflections)
+ relation
+ else
+ if relation.limit_value
+ limited_ids = limited_ids_for(relation)
+ limited_ids.empty? ? relation.none! : relation.where!(primary_key => limited_ids)
+ end
+ relation.except(:limit, :offset)
end
- relation.except(:limit, :offset)
end
- end
- def limited_ids_for(relation)
- values = @klass.connection.columns_for_distinct(
- "#{quoted_table_name}.#{quoted_primary_key}", relation.order_values)
+ def limited_ids_for(relation)
+ values = @klass.connection.columns_for_distinct(
+ "#{quoted_table_name}.#{quoted_primary_key}", relation.order_values)
- relation = relation.except(:select).select(values).distinct!
- arel = relation.arel
+ relation = relation.except(:select).select(values).distinct!
+ arel = relation.arel
- id_rows = @klass.connection.select_all(arel, 'SQL', relation.bound_attributes)
- id_rows.map {|row| row[primary_key]}
- end
+ id_rows = @klass.connection.select_all(arel, "SQL", relation.bound_attributes)
+ id_rows.map { |row| row[primary_key] }
+ end
- def using_limitable_reflections?(reflections)
- reflections.none?(&:collection?)
- end
+ def using_limitable_reflections?(reflections)
+ reflections.none?(&:collection?)
+ end
protected
- def find_with_ids(*ids)
- raise UnknownPrimaryKey.new(@klass) if primary_key.nil?
+ def find_with_ids(*ids)
+ raise UnknownPrimaryKey.new(@klass) if primary_key.nil?
- expects_array = ids.first.kind_of?(Array)
- return ids.first if expects_array && ids.first.empty?
+ expects_array = ids.first.kind_of?(Array)
+ return ids.first if expects_array && ids.first.empty?
- ids = ids.flatten.compact.uniq
+ ids = ids.flatten.compact.uniq
- case ids.size
- when 0
- raise RecordNotFound, "Couldn't find #{@klass.name} without an ID"
- when 1
- result = find_one(ids.first)
- expects_array ? [ result ] : result
- else
- find_some(ids)
+ case ids.size
+ when 0
+ raise RecordNotFound, "Couldn't find #{@klass.name} without an ID"
+ when 1
+ result = find_one(ids.first)
+ expects_array ? [ result ] : result
+ else
+ find_some(ids)
+ end
+ rescue RangeError
+ raise RecordNotFound, "Couldn't find #{@klass.name} with an out of range ID"
end
- rescue RangeError
- raise RecordNotFound, "Couldn't find #{@klass.name} with an out of range ID"
- end
- def find_one(id)
- if ActiveRecord::Base === id
- id = id.id
- ActiveSupport::Deprecation.warn(<<-MSG.squish)
- You are passing an instance of ActiveRecord::Base to `find`.
- Please pass the id of the object by calling `.id`.
- MSG
- end
+ def find_one(id)
+ if ActiveRecord::Base === id
+ id = id.id
+ ActiveSupport::Deprecation.warn(<<-MSG.squish)
+ You are passing an instance of ActiveRecord::Base to `find`.
+ Please pass the id of the object by calling `.id`.
+ MSG
+ end
- relation = where(primary_key => id)
- record = relation.take
+ relation = where(primary_key => id)
+ record = relation.take
- raise_record_not_found_exception!(id, 0, 1) unless record
+ raise_record_not_found_exception!(id, 0, 1) unless record
- record
- end
+ record
+ end
- def find_some(ids)
- return find_some_ordered(ids) unless order_values.present?
+ def find_some(ids)
+ return find_some_ordered(ids) unless order_values.present?
- result = where(primary_key => ids).to_a
+ result = where(primary_key => ids).to_a
- expected_size =
- if limit_value && ids.size > limit_value
- limit_value
- else
- ids.size
- end
+ expected_size =
+ if limit_value && ids.size > limit_value
+ limit_value
+ else
+ ids.size
+ end
- # 11 ids with limit 3, offset 9 should give 2 results.
- if offset_value && (ids.size - offset_value < expected_size)
- expected_size = ids.size - offset_value
- end
+ # 11 ids with limit 3, offset 9 should give 2 results.
+ if offset_value && (ids.size - offset_value < expected_size)
+ expected_size = ids.size - offset_value
+ end
- if result.size == expected_size
- result
- else
- raise_record_not_found_exception!(ids, result.size, expected_size)
+ if result.size == expected_size
+ result
+ else
+ raise_record_not_found_exception!(ids, result.size, expected_size)
+ end
end
- end
- def find_some_ordered(ids)
- ids = ids.slice(offset_value || 0, limit_value || ids.size) || []
+ def find_some_ordered(ids)
+ ids = ids.slice(offset_value || 0, limit_value || ids.size) || []
- result = except(:limit, :offset).where(primary_key => ids).records
+ result = except(:limit, :offset).where(primary_key => ids).records
- if result.size == ids.size
- pk_type = @klass.type_for_attribute(primary_key)
+ if result.size == ids.size
+ pk_type = @klass.type_for_attribute(primary_key)
- records_by_id = result.index_by(&:id)
- ids.map { |id| records_by_id.fetch(pk_type.cast(id)) }
- else
- raise_record_not_found_exception!(ids, result.size, ids.size)
+ records_by_id = result.index_by(&:id)
+ ids.map { |id| records_by_id.fetch(pk_type.cast(id)) }
+ else
+ raise_record_not_found_exception!(ids, result.size, ids.size)
+ end
end
- end
- def find_take
- if loaded?
- @records.first
- else
- @take ||= limit(1).records.first
+ def find_take
+ if loaded?
+ records.first
+ else
+ @take ||= limit(1).records.first
+ end
end
- end
- def find_nth(index, offset = nil)
- # 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
- end
- if loaded?
- @records[index]
- else
- offset ||= offset_index
- @offsets[offset + index] ||= find_nth_with_limit_and_offset(index, 1, offset: offset).first
+ def find_take_with_limit(limit)
+ if loaded?
+ records.take(limit)
+ else
+ limit(limit).to_a
+ end
end
- end
- def find_nth!(index)
- find_nth(index) or raise RecordNotFound.new("Couldn't find #{@klass.name} with [#{arel.where_sql(@klass.arel_engine)}]")
- end
+ def find_nth(index)
+ @offsets[offset_index + index] ||= find_nth_with_limit(index, 1).first
+ end
- 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_attribute(primary_key).asc)
- else
- self
- end
-
- relation = relation.offset(index) unless index.zero?
- relation.limit(limit).to_a
- end
+ def find_nth_with_limit(index, limit)
+ if loaded?
+ records[index, limit] || []
+ else
+ relation = if order_values.empty? && primary_key
+ order(arel_attribute(primary_key).asc)
+ else
+ self
+ end
+
+ relation = relation.offset(offset_index + index) unless index.zero?
+ relation.limit(limit).to_a
+ end
+ end
- def find_nth_from_last(index)
- if loaded?
- @records[-index]
- else
- 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
+ def find_nth_from_last(index)
+ if loaded?
+ records[-index]
+ else
+ 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
- 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)
+ def find_last(limit)
+ limit ? records.last(limit) : records.last
end
- end
-
- def find_last(limit)
- limit ? records.last(limit) : records.last
- end
end
end
diff --git a/activerecord/lib/active_record/relation/merger.rb b/activerecord/lib/active_record/relation/merger.rb
index 396638d74d..5dac00724a 100644
--- a/activerecord/lib/active_record/relation/merger.rb
+++ b/activerecord/lib/active_record/relation/merger.rb
@@ -1,4 +1,4 @@
-require 'active_support/core_ext/hash/keys'
+require "active_support/core_ext/hash/keys"
module ActiveRecord
class Relation
@@ -83,85 +83,81 @@ module ActiveRecord
private
- def merge_preloads
- return if other.preload_values.empty? && other.includes_values.empty?
+ def merge_preloads
+ return if other.preload_values.empty? && other.includes_values.empty?
- if other.klass == relation.klass
- relation.preload!(*other.preload_values) unless other.preload_values.empty?
- relation.includes!(other.includes_values) unless other.includes_values.empty?
- else
- reflection = relation.klass.reflect_on_all_associations.find do |r|
- r.class_name == other.klass.name
- end || return
+ if other.klass == relation.klass
+ relation.preload!(*other.preload_values) unless other.preload_values.empty?
+ relation.includes!(other.includes_values) unless other.includes_values.empty?
+ else
+ reflection = relation.klass.reflect_on_all_associations.find do |r|
+ r.class_name == other.klass.name
+ end || return
- unless other.preload_values.empty?
- relation.preload! reflection.name => other.preload_values
- end
+ unless other.preload_values.empty?
+ relation.preload! reflection.name => other.preload_values
+ end
- unless other.includes_values.empty?
- relation.includes! reflection.name => other.includes_values
+ unless other.includes_values.empty?
+ relation.includes! reflection.name => other.includes_values
+ end
end
end
- end
- def merge_joins
- return if other.joins_values.blank?
+ def merge_joins
+ return if other.joins_values.blank?
- if other.klass == relation.klass
- relation.joins!(*other.joins_values)
- else
- joins_dependency, rest = other.joins_values.partition do |join|
- case join
- when Hash, Symbol, Array
- true
- else
- false
+ if other.klass == relation.klass
+ relation.joins!(*other.joins_values)
+ else
+ joins_dependency, rest = other.joins_values.partition do |join|
+ case join
+ when Hash, Symbol, Array
+ true
+ else
+ false
+ end
end
- end
-
- join_dependency = ActiveRecord::Associations::JoinDependency.new(other.klass,
- joins_dependency,
- [])
- relation.joins! rest
- @relation = relation.joins join_dependency
- end
- end
+ join_dependency = ActiveRecord::Associations::JoinDependency.new(other.klass,
+ joins_dependency,
+ [])
+ relation.joins! rest
- def merge_multi_values
- if other.reordering_value
- # override any order specified in the original relation
- relation.reorder! other.order_values
- elsif other.order_values
- # merge in order_values from relation
- relation.order! other.order_values
+ @relation = relation.joins join_dependency
+ end
end
- relation.extend(*other.extending_values) unless other.extending_values.blank?
- end
+ def merge_multi_values
+ if other.reordering_value
+ # override any order specified in the original relation
+ relation.reorder! other.order_values
+ elsif other.order_values
+ # merge in order_values from relation
+ relation.order! other.order_values
+ end
- def merge_single_values
- if relation.from_clause.empty?
- relation.from_clause = other.from_clause
+ relation.extend(*other.extending_values) unless other.extending_values.blank?
end
- relation.lock_value ||= other.lock_value
- unless other.create_with_value.blank?
- relation.create_with_value = (relation.create_with_value || {}).merge(other.create_with_value)
- end
- end
+ def merge_single_values
+ if relation.from_clause.empty?
+ relation.from_clause = other.from_clause
+ end
+ relation.lock_value ||= other.lock_value
- CLAUSE_METHOD_NAMES = CLAUSE_METHODS.map do |name|
- ["#{name}_clause", "#{name}_clause="]
- end
+ unless other.create_with_value.blank?
+ relation.create_with_value = (relation.create_with_value || {}).merge(other.create_with_value)
+ end
+ end
- def merge_clauses
- CLAUSE_METHOD_NAMES.each do |(reader, writer)|
- clause = relation.send(reader)
- other_clause = other.send(reader)
- relation.send(writer, clause.merge(other_clause))
+ def merge_clauses
+ CLAUSE_METHODS.each do |method|
+ clause = relation.get_value(method)
+ other_clause = other.get_value(method)
+ relation.set_value(method, clause.merge(other_clause))
+ end
end
- end
end
end
end
diff --git a/activerecord/lib/active_record/relation/predicate_builder.rb b/activerecord/lib/active_record/relation/predicate_builder.rb
index ecce949370..780a1ee422 100644
--- a/activerecord/lib/active_record/relation/predicate_builder.rb
+++ b/activerecord/lib/active_record/relation/predicate_builder.rb
@@ -1,13 +1,13 @@
module ActiveRecord
class PredicateBuilder # :nodoc:
- require 'active_record/relation/predicate_builder/array_handler'
- require 'active_record/relation/predicate_builder/association_query_handler'
- 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'
+ require "active_record/relation/predicate_builder/array_handler"
+ require "active_record/relation/predicate_builder/association_query_handler"
+ 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"
delegate :resolve_column_aliases, to: :table
@@ -15,11 +15,11 @@ module ActiveRecord
@table = table
@handlers = []
- register_handler(BasicObject, BasicObjectHandler.new(self))
+ register_handler(BasicObject, BasicObjectHandler.new)
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(Range, RangeHandler.new)
+ register_handler(RangeHandler::RangeWithBinds, RangeHandler.new)
register_handler(Relation, RelationHandler.new)
register_handler(Array, ArrayHandler.new(self))
register_handler(AssociationQueryValue, AssociationQueryHandler.new(self))
@@ -36,23 +36,13 @@ module ActiveRecord
create_binds_for_hash(attributes)
end
- def expand(column, value)
- # Find the foreign key when using queries such as:
- # Post.where(author: author)
- #
- # For polymorphic relationships, find the foreign key and type:
- # PriceEstimate.where(estimate_of: treasure)
- value = AssociationQueryHandler.value_for(table, column, value) if table.associated_with?(column)
- build(table.arel_attribute(column), value)
- end
-
def self.references(attributes)
attributes.map do |key, value|
if value.is_a?(Hash)
key
else
key = key.to_s
- key.split('.'.freeze).first if key.include?('.'.freeze)
+ key.split(".".freeze).first if key.include?(".".freeze)
end
end.compact
end
@@ -78,91 +68,104 @@ module ActiveRecord
protected
- attr_reader :table
+ attr_reader :table
- def expand_from_hash(attributes)
- return ["1=0"] if attributes.empty?
+ def expand_from_hash(attributes)
+ return ["1=0"] if attributes.empty?
- attributes.flat_map do |key, value|
- if value.is_a?(Hash)
- associated_predicate_builder(key).expand_from_hash(value)
- else
- expand(key, value)
+ attributes.flat_map do |key, value|
+ if value.is_a?(Hash) && !table.has_column?(key)
+ associated_predicate_builder(key).expand_from_hash(value)
+ else
+ build(table.arel_attribute(key), value)
+ end
end
end
- end
-
-
- def create_binds_for_hash(attributes)
- result = attributes.dup
- binds = []
- attributes.each do |column_name, value|
- case value
- when Hash
- attrs, bvs = associated_predicate_builder(column_name).create_binds_for_hash(value)
- result[column_name] = attrs
- 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
+ def create_binds_for_hash(attributes)
+ result = attributes.dup
+ binds = []
+
+ attributes.each do |column_name, value|
+ case
+ when value.is_a?(Hash) && !table.has_column?(column_name)
+ attrs, bvs = associated_predicate_builder(column_name).create_binds_for_hash(value)
+ result[column_name] = attrs
+ binds += bvs
+ next
+ when value.is_a?(Relation)
+ binds += value.bound_attributes
+ when value.is_a?(Range) && !table.type(column_name).respond_to?(:subtype)
+ 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 << build_bind_param(column_name, value)
+ end
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 << build_bind_param(column_name, value)
+ # Find the foreign key when using queries such as:
+ # Post.where(author: author)
+ #
+ # For polymorphic relationships, find the foreign key and type:
+ # PriceEstimate.where(estimate_of: treasure)
+ if table.associated_with?(column_name)
+ result[column_name] = AssociationQueryHandler.value_for(table, column_name, value)
end
end
- end
- [result, binds]
- end
+ [result, binds]
+ end
private
- def associated_predicate_builder(association_name)
- self.class.new(table.associated_table(association_name))
- end
-
- def convert_dot_notation_to_hash(attributes)
- dot_notation = attributes.select do |k, v|
- k.include?(".".freeze) && !v.is_a?(Hash)
+ def associated_predicate_builder(association_name)
+ self.class.new(table.associated_table(association_name))
end
- dot_notation.each_key do |key|
- table_name, column_name = key.split(".".freeze)
- value = attributes.delete(key)
- attributes[table_name] ||= {}
+ def convert_dot_notation_to_hash(attributes)
+ dot_notation = attributes.select do |k, v|
+ k.include?(".".freeze) && !v.is_a?(Hash)
+ end
- attributes[table_name] = attributes[table_name].merge(column_name => value)
- end
+ dot_notation.each_key do |key|
+ table_name, column_name = key.split(".".freeze)
+ value = attributes.delete(key)
+ attributes[table_name] ||= {}
- attributes
- end
+ attributes[table_name] = attributes[table_name].merge(column_name => value)
+ end
- def handler_for(object)
- @handlers.detect { |klass, _| klass === object }.last
- end
+ attributes
+ end
- def can_be_bound?(column_name, value)
- !value.nil? &&
- handler_for(value).is_a?(BasicObjectHandler) &&
- !table.associated_with?(column_name)
- end
+ def handler_for(object)
+ @handlers.detect { |klass, _| klass === object }.last
+ end
- def build_bind_param(column_name, value)
- Relation::QueryAttribute.new(column_name.to_s, value, table.type(column_name))
- end
+ def can_be_bound?(column_name, value)
+ return if table.associated_with?(column_name)
+ case value
+ when Array, Range
+ table.type(column_name).respond_to?(:subtype)
+ else
+ !value.nil? && handler_for(value).is_a?(BasicObjectHandler)
+ end
+ 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/array_handler.rb b/activerecord/lib/active_record/relation/predicate_builder/array_handler.rb
index 95dbd6a77f..6400caba06 100644
--- a/activerecord/lib/active_record/relation/predicate_builder/array_handler.rb
+++ b/activerecord/lib/active_record/relation/predicate_builder/array_handler.rb
@@ -31,13 +31,13 @@ module ActiveRecord
protected
- attr_reader :predicate_builder
+ attr_reader :predicate_builder
- module NullPredicate # :nodoc:
- def self.or(other)
- other
+ module NullPredicate # :nodoc:
+ def self.or(other)
+ other
+ end
end
- end
end
end
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 413cb9fd84..7e20cb2c63 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
@@ -30,7 +30,7 @@ module ActiveRecord
protected
- attr_reader :predicate_builder
+ attr_reader :predicate_builder
end
class AssociationQueryValue # :nodoc:
@@ -60,30 +60,30 @@ module ActiveRecord
private
- def primary_key
- associated_table.association_primary_key(base_class)
- end
+ def primary_key
+ associated_table.association_primary_key(base_class)
+ end
- def polymorphic_base_class_from_value
- case value
- when Relation
- value.klass.base_class
- when Array
- val = value.compact.first
- val.class.base_class if val.is_a?(Base)
- when Base
- value.class.base_class
+ def polymorphic_base_class_from_value
+ case value
+ when Relation
+ value.klass.base_class
+ when Array
+ val = value.compact.first
+ val.class.base_class if val.is_a?(Base)
+ when Base
+ value.class.base_class
+ end
end
- end
- def convert_to_id(value)
- case value
- when Base
- value._read_attribute(primary_key)
- else
- value
+ def convert_to_id(value)
+ case value
+ when Base
+ value._read_attribute(primary_key)
+ else
+ value
+ end
end
- end
end
end
end
diff --git a/activerecord/lib/active_record/relation/predicate_builder/base_handler.rb b/activerecord/lib/active_record/relation/predicate_builder/base_handler.rb
index 6fa5b16f73..65c5159704 100644
--- a/activerecord/lib/active_record/relation/predicate_builder/base_handler.rb
+++ b/activerecord/lib/active_record/relation/predicate_builder/base_handler.rb
@@ -11,7 +11,7 @@ module ActiveRecord
protected
- attr_reader :predicate_builder
+ attr_reader :predicate_builder
end
end
end
diff --git a/activerecord/lib/active_record/relation/predicate_builder/basic_object_handler.rb b/activerecord/lib/active_record/relation/predicate_builder/basic_object_handler.rb
index 6cec75dc0a..79cde00303 100644
--- a/activerecord/lib/active_record/relation/predicate_builder/basic_object_handler.rb
+++ b/activerecord/lib/active_record/relation/predicate_builder/basic_object_handler.rb
@@ -1,17 +1,9 @@
module ActiveRecord
class PredicateBuilder
class BasicObjectHandler # :nodoc:
- def initialize(predicate_builder)
- @predicate_builder = predicate_builder
- end
-
def call(attribute, value)
attribute.eq(value)
end
-
- protected
-
- attr_reader :predicate_builder
end
end
end
diff --git a/activerecord/lib/active_record/relation/predicate_builder/class_handler.rb b/activerecord/lib/active_record/relation/predicate_builder/class_handler.rb
index ed313fc9d4..0a6574fcf1 100644
--- a/activerecord/lib/active_record/relation/predicate_builder/class_handler.rb
+++ b/activerecord/lib/active_record/relation/predicate_builder/class_handler.rb
@@ -12,16 +12,16 @@ module ActiveRecord
protected
- attr_reader :predicate_builder
+ attr_reader :predicate_builder
private
- def print_deprecation_warning
- ActiveSupport::Deprecation.warn(<<-MSG.squish)
- Passing a class as a value in an Active Record query is deprecated and
- will be removed. Pass a string instead.
- MSG
- end
+ def print_deprecation_warning
+ ActiveSupport::Deprecation.warn(<<-MSG.squish)
+ Passing a class as a value in an Active Record query is deprecated and
+ will be removed. Pass a string instead.
+ MSG
+ end
end
end
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
index b6c6240343..0c7f92b3d0 100644
--- a/activerecord/lib/active_record/relation/predicate_builder/polymorphic_array_handler.rb
+++ b/activerecord/lib/active_record/relation/predicate_builder/polymorphic_array_handler.rb
@@ -23,7 +23,7 @@ module ActiveRecord
protected
- attr_reader :predicate_builder
+ attr_reader :predicate_builder
end
class PolymorphicArrayValue # :nodoc:
@@ -41,17 +41,17 @@ module ActiveRecord
private
- def primary_key(value)
- associated_table.association_primary_key(base_class(value))
- end
+ def primary_key(value)
+ associated_table.association_primary_key(base_class(value))
+ end
- def base_class(value)
- value.class.base_class
- end
+ def base_class(value)
+ value.class.base_class
+ end
- def convert_to_id(value)
- value._read_attribute(primary_key(value))
- 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/range_handler.rb b/activerecord/lib/active_record/relation/predicate_builder/range_handler.rb
index 306d4694ae..5db778e19c 100644
--- a/activerecord/lib/active_record/relation/predicate_builder/range_handler.rb
+++ b/activerecord/lib/active_record/relation/predicate_builder/range_handler.rb
@@ -3,10 +3,6 @@ module ActiveRecord
class RangeHandler # :nodoc:
RangeWithBinds = Struct.new(:begin, :end, :exclude_end?)
- def initialize(predicate_builder)
- @predicate_builder = predicate_builder
- end
-
def call(attribute, value)
if value.begin.respond_to?(:infinite?) && value.begin.infinite?
if value.end.respond_to?(:infinite?) && value.end.infinite?
@@ -24,10 +20,6 @@ module ActiveRecord
attribute.between(value)
end
end
-
- protected
-
- attr_reader :predicate_builder
end
end
end
diff --git a/activerecord/lib/active_record/relation/query_attribute.rb b/activerecord/lib/active_record/relation/query_attribute.rb
index 7ba964e802..a68e508fcc 100644
--- a/activerecord/lib/active_record/relation/query_attribute.rb
+++ b/activerecord/lib/active_record/relation/query_attribute.rb
@@ -1,4 +1,4 @@
-require 'active_record/attribute'
+require "active_record/attribute"
module ActiveRecord
class Relation
diff --git a/activerecord/lib/active_record/relation/query_methods.rb b/activerecord/lib/active_record/relation/query_methods.rb
index 8a87015e44..c6d0902e0d 100644
--- a/activerecord/lib/active_record/relation/query_methods.rb
+++ b/activerecord/lib/active_record/relation/query_methods.rb
@@ -2,8 +2,9 @@ require "active_record/relation/from_clause"
require "active_record/relation/query_attribute"
require "active_record/relation/where_clause"
require "active_record/relation/where_clause_factory"
-require 'active_model/forbidden_attributes_protection'
-require 'active_support/core_ext/string/filters'
+require "active_model/forbidden_attributes_protection"
+require "active_support/core_ext/string/filters"
+require "active_support/core_ext/regexp"
module ActiveRecord
module QueryMethods
@@ -55,62 +56,39 @@ module ActiveRecord
end
FROZEN_EMPTY_ARRAY = [].freeze
- Relation::MULTI_VALUE_METHODS.each do |name|
- class_eval <<-CODE, __FILE__, __LINE__ + 1
- def #{name}_values
- @values[:#{name}] || FROZEN_EMPTY_ARRAY
- end
+ FROZEN_EMPTY_HASH = {}.freeze
- def #{name}_values=(values)
- assert_mutability!
- @values[:#{name}] = values
+ Relation::VALUE_METHODS.each do |name|
+ method_name = \
+ case name
+ when *Relation::MULTI_VALUE_METHODS then "#{name}_values"
+ when *Relation::SINGLE_VALUE_METHODS then "#{name}_value"
+ when *Relation::CLAUSE_METHODS then "#{name}_clause"
end
- CODE
- end
-
- (Relation::SINGLE_VALUE_METHODS - [:create_with]).each do |name|
class_eval <<-CODE, __FILE__, __LINE__ + 1
- def #{name}_value # def readonly_value
- @values[:#{name}] # @values[:readonly]
+ def #{method_name} # def includes_values
+ get_value(#{name.inspect}) # get_value(:includes)
end # end
- CODE
- end
- Relation::SINGLE_VALUE_METHODS.each do |name|
- class_eval <<-CODE, __FILE__, __LINE__ + 1
- def #{name}_value=(value) # def readonly_value=(value)
- assert_mutability! # assert_mutability!
- @values[:#{name}] = value # @values[:readonly] = value
+ def #{method_name}=(value) # def includes_values=(value)
+ set_value(#{name.inspect}, value) # set_value(:includes, value)
end # end
CODE
end
- Relation::CLAUSE_METHODS.each do |name|
- class_eval <<-CODE, __FILE__, __LINE__ + 1
- def #{name}_clause # def where_clause
- @values[:#{name}] || new_#{name}_clause # @values[:where] || new_where_clause
- end # end
- #
- def #{name}_clause=(value) # def where_clause=(value)
- assert_mutability! # assert_mutability!
- @values[:#{name}] = value # @values[:where] = value
- end # end
- CODE
- end
-
def bound_attributes
if limit_value && !string_containing_comma?(limit_value)
limit_bind = Attribute.with_cast_value(
"LIMIT".freeze,
connection.sanitize_limit(limit_value),
- Type::Value.new,
+ Type.default_value,
)
end
if offset_value
offset_bind = Attribute.with_cast_value(
"OFFSET".freeze,
offset_value.to_i,
- Type::Value.new,
+ Type.default_value,
)
end
connection.combine_bind_parameters(
@@ -123,11 +101,6 @@ module ActiveRecord
)
end
- FROZEN_EMPTY_HASH = {}.freeze
- def create_with_value # :nodoc:
- @values[:create_with] || FROZEN_EMPTY_HASH
- end
-
alias extensions extending_values
# Specify relationships to be included in the result set. For
@@ -269,8 +242,17 @@ module ActiveRecord
# Model.select(:field).first.other_field
# # => ActiveModel::MissingAttributeError: missing attribute: other_field
def select(*fields)
- return super if block_given?
- raise ArgumentError, 'Call this with at least one field' if fields.empty?
+ if block_given?
+ if fields.any?
+ ActiveSupport::Deprecation.warn(<<-WARNING.squish)
+ When select is called with a block, it ignores other arguments. This behavior is now deprecated and will result in an ArgumentError in Rails 5.1. You can safely remove the arguments to resolve the deprecation warning because they do not have any effect on the output of the call to the select method with a block.
+ WARNING
+ end
+
+ return super()
+ end
+
+ raise ArgumentError, "Call this with at least one field" if fields.empty?
spawn._select!(*fields)
end
@@ -417,7 +399,10 @@ module ActiveRecord
args.each do |scope|
case scope
when Symbol
- symbol_unscoping(scope)
+ if !VALID_UNSCOPING_VALUES.include?(scope)
+ raise ArgumentError, "Called unscope() with invalid unscoping argument ':#{scope}'. Valid arguments are :#{VALID_UNSCOPING_VALUES.to_a.join(", :")}."
+ end
+ set_value(scope, nil)
when Hash
scope.each do |key, target_value|
if key != :where
@@ -495,7 +480,6 @@ module ActiveRecord
self.left_outer_joins_values += args
self
end
- alias :left_joins! :left_outer_joins!
# Returns a new relation, which is the result of filtering the current relation
# according to the conditions in the arguments.
@@ -949,288 +933,287 @@ module ActiveRecord
@arel ||= build_arel
end
- private
-
- def assert_mutability!
- raise ImmutableRelation if @loaded
- raise ImmutableRelation if defined?(@arel) && @arel
+ # Returns a relation value with a given name
+ def get_value(name) # :nodoc:
+ @values[name] || default_value_for(name)
end
- def build_arel
- arel = Arel::SelectManager.new(table)
+ # Sets the relation value with the given name
+ def set_value(name, value) # :nodoc:
+ assert_mutability!
+ @values[name] = value
+ end
- build_joins(arel, joins_values.flatten) unless joins_values.empty?
- build_left_outer_joins(arel, left_outer_joins_values.flatten) unless left_outer_joins_values.empty?
+ private
- arel.where(where_clause.ast) unless where_clause.empty?
- arel.having(having_clause.ast) unless having_clause.empty?
- if limit_value
- if string_containing_comma?(limit_value)
- arel.take(connection.sanitize_limit(limit_value))
- else
- arel.take(Arel::Nodes::BindParam.new)
- end
+ def assert_mutability!
+ raise ImmutableRelation if @loaded
+ raise ImmutableRelation if defined?(@arel) && @arel
end
- arel.skip(Arel::Nodes::BindParam.new) if offset_value
- arel.group(*arel_columns(group_values.uniq.reject(&:blank?))) unless group_values.empty?
- build_order(arel)
+ def build_arel
+ arel = Arel::SelectManager.new(table)
- build_select(arel)
+ build_joins(arel, joins_values.flatten) unless joins_values.empty?
+ build_left_outer_joins(arel, left_outer_joins_values.flatten) unless left_outer_joins_values.empty?
- arel.distinct(distinct_value)
- arel.from(build_from) unless from_clause.empty?
- arel.lock(lock_value) if lock_value
+ arel.where(where_clause.ast) unless where_clause.empty?
+ arel.having(having_clause.ast) unless having_clause.empty?
+ if limit_value
+ if string_containing_comma?(limit_value)
+ arel.take(connection.sanitize_limit(limit_value))
+ else
+ arel.take(Arel::Nodes::BindParam.new)
+ end
+ end
+ arel.skip(Arel::Nodes::BindParam.new) if offset_value
+ arel.group(*arel_columns(group_values.uniq.reject(&:blank?))) unless group_values.empty?
- arel
- end
+ build_order(arel)
- def symbol_unscoping(scope)
- if !VALID_UNSCOPING_VALUES.include?(scope)
- raise ArgumentError, "Called unscope() with invalid unscoping argument ':#{scope}'. Valid arguments are :#{VALID_UNSCOPING_VALUES.to_a.join(", :")}."
- end
+ build_select(arel)
- clause_method = Relation::CLAUSE_METHODS.include?(scope)
- multi_val_method = Relation::MULTI_VALUE_METHODS.include?(scope)
- if clause_method
- unscope_code = "#{scope}_clause="
- else
- unscope_code = "#{scope}_value#{'s' if multi_val_method}="
- end
+ arel.distinct(distinct_value)
+ arel.from(build_from) unless from_clause.empty?
+ arel.lock(lock_value) if lock_value
- case scope
- when :order
- result = []
- else
- result = [] if multi_val_method
+ arel
end
- self.send(unscope_code, result)
- end
-
- def build_from
- opts = from_clause.value
- name = from_clause.name
- case opts
- when Relation
- name ||= 'subquery'
- opts.arel.as(name.to_s)
- else
- opts
- end
- end
-
- def build_left_outer_joins(manager, outer_joins)
- buckets = outer_joins.group_by do |join|
- case join
- when Hash, Symbol, Array
- :association_join
+ def build_from
+ opts = from_clause.value
+ name = from_clause.name
+ case opts
+ when Relation
+ name ||= "subquery"
+ opts.arel.as(name.to_s)
else
- raise ArgumentError, 'only Hash, Symbol and Array are allowed'
+ opts
end
end
- build_join_query(manager, buckets, Arel::Nodes::OuterJoin)
- end
-
- def build_joins(manager, joins)
- buckets = joins.group_by do |join|
- case join
- when String
- :string_join
- when Hash, Symbol, Array
- :association_join
- when ActiveRecord::Associations::JoinDependency
- :stashed_join
- when Arel::Nodes::Join
- :join_node
- else
- raise 'unknown class: %s' % join.class.name
+ def build_left_outer_joins(manager, outer_joins)
+ buckets = outer_joins.group_by do |join|
+ case join
+ when Hash, Symbol, Array
+ :association_join
+ else
+ raise ArgumentError, "only Hash, Symbol and Array are allowed"
+ end
end
+
+ build_join_query(manager, buckets, Arel::Nodes::OuterJoin)
end
- build_join_query(manager, buckets, Arel::Nodes::InnerJoin)
- end
+ def build_joins(manager, joins)
+ buckets = joins.group_by do |join|
+ case join
+ when String
+ :string_join
+ when Hash, Symbol, Array
+ :association_join
+ when ActiveRecord::Associations::JoinDependency
+ :stashed_join
+ when Arel::Nodes::Join
+ :join_node
+ else
+ raise "unknown class: %s" % join.class.name
+ end
+ end
- def build_join_query(manager, buckets, join_type)
- buckets.default = []
+ build_join_query(manager, buckets, Arel::Nodes::InnerJoin)
+ end
- association_joins = buckets[:association_join]
- stashed_association_joins = buckets[:stashed_join]
- join_nodes = buckets[:join_node].uniq
- string_joins = buckets[:string_join].map(&:strip).uniq
+ def build_join_query(manager, buckets, join_type)
+ buckets.default = []
- join_list = join_nodes + convert_join_strings_to_ast(manager, string_joins)
+ association_joins = buckets[:association_join]
+ stashed_association_joins = buckets[:stashed_join]
+ join_nodes = buckets[:join_node].uniq
+ string_joins = buckets[:string_join].map(&:strip).uniq
- join_dependency = ActiveRecord::Associations::JoinDependency.new(
- @klass,
- association_joins,
- join_list
- )
+ join_list = join_nodes + convert_join_strings_to_ast(manager, string_joins)
- join_infos = join_dependency.join_constraints stashed_association_joins, join_type
+ join_dependency = ActiveRecord::Associations::JoinDependency.new(
+ @klass,
+ association_joins,
+ join_list
+ )
- join_infos.each do |info|
- info.joins.each { |join| manager.from(join) }
- manager.bind_values.concat info.binds
- end
+ join_infos = join_dependency.join_constraints stashed_association_joins, join_type
- manager.join_sources.concat(join_list)
+ join_infos.each do |info|
+ info.joins.each { |join| manager.from(join) }
+ manager.bind_values.concat info.binds
+ end
- manager
- end
+ manager.join_sources.concat(join_list)
- def convert_join_strings_to_ast(table, joins)
- joins
- .flatten
- .reject(&:blank?)
- .map { |join| table.create_string_join(Arel.sql(join)) }
- end
+ manager
+ end
- def build_select(arel)
- if select_values.any?
- arel.project(*arel_columns(select_values.uniq))
- else
- arel.project(@klass.arel_table[Arel.star])
+ def convert_join_strings_to_ast(table, joins)
+ joins
+ .flatten
+ .reject(&:blank?)
+ .map { |join| table.create_string_join(Arel.sql(join)) }
end
- end
- def arel_columns(columns)
- columns.map do |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)
+ def build_select(arel)
+ if select_values.any?
+ arel.project(*arel_columns(select_values.uniq))
else
- field
+ arel.project(@klass.arel_table[Arel.star])
end
end
- end
- def reverse_sql_order(order_query)
- if order_query.empty?
- 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"
+ def arel_columns(columns)
+ columns.map do |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
+ field
+ end
+ end
end
- order_query.flat_map do |o|
- case o
- when Arel::Attribute
- o.desc
- when Arel::Nodes::Ordering
- o.reverse
- when String
- if does_not_support_reverse?(o)
- raise IrreversibleOrderError, "Order #{o.inspect} can not be reversed automatically"
- end
- o.split(',').map! do |s|
- s.strip!
- s.gsub!(/\sasc\Z/i, ' DESC') || s.gsub!(/\sdesc\Z/i, ' ASC') || s.concat(' DESC')
+ def reverse_sql_order(order_query)
+ if order_query.empty?
+ 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
+ if does_not_support_reverse?(o)
+ raise IrreversibleOrderError, "Order #{o.inspect} can not be reversed automatically"
+ end
+ o.split(",").map! do |s|
+ s.strip!
+ s.gsub!(/\sasc\Z/i, " DESC") || s.gsub!(/\sdesc\Z/i, " ASC") || s.concat(" DESC")
+ end
+ else
+ o
end
- else
- o
end
end
- end
- def does_not_support_reverse?(order)
- #uses sql function with multiple arguments
- order =~ /\([^()]*,[^()]*\)/ ||
- # uses "nulls first" like construction
- order =~ /nulls (first|last)\Z/i
- end
+ def does_not_support_reverse?(order)
+ # Uses SQL function with multiple arguments.
+ (order.include?(",") && order.split(",").find { |section| section.count("(") != section.count(")") }) ||
+ # Uses "nulls first" like construction.
+ /nulls (first|last)\Z/i.match?(order)
+ end
- def build_order(arel)
- orders = order_values.uniq
- orders.reject!(&:blank?)
+ def build_order(arel)
+ orders = order_values.uniq
+ orders.reject!(&:blank?)
- arel.order(*orders) unless orders.empty?
- end
+ arel.order(*orders) unless orders.empty?
+ end
- VALID_DIRECTIONS = [:asc, :desc, :ASC, :DESC,
- 'asc', 'desc', 'ASC', 'DESC'] # :nodoc:
+ VALID_DIRECTIONS = [:asc, :desc, :ASC, :DESC,
+ "asc", "desc", "ASC", "DESC"] # :nodoc:
- def validate_order_args(args)
- args.each do |arg|
- next unless arg.is_a?(Hash)
- arg.each do |_key, value|
- raise ArgumentError, "Direction \"#{value}\" is invalid. Valid " \
- "directions are: #{VALID_DIRECTIONS.inspect}" unless VALID_DIRECTIONS.include?(value)
+ def validate_order_args(args)
+ args.each do |arg|
+ next unless arg.is_a?(Hash)
+ arg.each do |_key, value|
+ raise ArgumentError, "Direction \"#{value}\" is invalid. Valid " \
+ "directions are: #{VALID_DIRECTIONS.inspect}" unless VALID_DIRECTIONS.include?(value)
+ end
end
end
- end
- def preprocess_order_args(order_args)
- order_args.map! do |arg|
- klass.send(:sanitize_sql_for_order, arg)
+ def preprocess_order_args(order_args)
+ order_args.map! do |arg|
+ klass.send(:sanitize_sql_for_order, arg)
+ end
+ order_args.flatten!
+ validate_order_args(order_args)
+
+ references = order_args.grep(String)
+ references.map! { |arg| arg =~ /^([a-zA-Z]\w*)\.(\w+)/ && $1 }.compact!
+ references!(references) if references.any?
+
+ # if a symbol is given we prepend the quoted table name
+ order_args.map! do |arg|
+ case arg
+ when Symbol
+ arel_attribute(arg).asc
+ when Hash
+ arg.map { |field, dir|
+ arel_attribute(field).send(dir.downcase)
+ }
+ else
+ arg
+ end
+ end.flatten!
end
- order_args.flatten!
- validate_order_args(order_args)
-
- references = order_args.grep(String)
- references.map! { |arg| arg =~ /^([a-zA-Z]\w*)\.(\w+)/ && $1 }.compact!
- references!(references) if references.any?
- # if a symbol is given we prepend the quoted table name
- order_args.map! do |arg|
- case arg
- when Symbol
- arel_attribute(arg).asc
- when Hash
- arg.map { |field, dir|
- arel_attribute(field).send(dir.downcase)
- }
- else
- arg
+ # Checks to make sure that the arguments are not blank. Note that if some
+ # blank-like object were initially passed into the query method, then this
+ # method will not raise an error.
+ #
+ # Example:
+ #
+ # Post.references() # raises an error
+ # Post.references([]) # does not raise an error
+ #
+ # This particular method should be called with a method_name and the args
+ # passed into that method as an input. For example:
+ #
+ # def references(*args)
+ # check_if_method_has_arguments!("references", args)
+ # ...
+ # end
+ def check_if_method_has_arguments!(method_name, args)
+ if args.blank?
+ raise ArgumentError, "The method .#{method_name}() must contain arguments."
end
- end.flatten!
- end
-
- # Checks to make sure that the arguments are not blank. Note that if some
- # blank-like object were initially passed into the query method, then this
- # method will not raise an error.
- #
- # Example:
- #
- # Post.references() # raises an error
- # Post.references([]) # does not raise an error
- #
- # This particular method should be called with a method_name and the args
- # passed into that method as an input. For example:
- #
- # def references(*args)
- # check_if_method_has_arguments!("references", args)
- # ...
- # end
- def check_if_method_has_arguments!(method_name, args)
- if args.blank?
- raise ArgumentError, "The method .#{method_name}() must contain arguments."
end
- end
- 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
- Relation::WhereClause.empty
- end
- alias new_having_clause new_where_clause
+ STRUCTURAL_OR_METHODS = Relation::VALUE_METHODS - [:extending, :where, :having]
+ def structurally_incompatible_values_for_or(other)
+ STRUCTURAL_OR_METHODS.reject do |method|
+ get_value(method) == other.get_value(method)
+ end
+ end
- def where_clause_factory
- @where_clause_factory ||= Relation::WhereClauseFactory.new(klass, predicate_builder)
- end
- alias having_clause_factory where_clause_factory
+ def where_clause_factory
+ @where_clause_factory ||= Relation::WhereClauseFactory.new(klass, predicate_builder)
+ end
+ alias having_clause_factory where_clause_factory
- def new_from_clause
- Relation::FromClause.empty
- end
+ def string_containing_comma?(value)
+ ::String === value && value.include?(",")
+ end
- def string_containing_comma?(value)
- ::String === value && value.include?(",")
- end
+ def default_value_for(name)
+ case name
+ when :create_with
+ FROZEN_EMPTY_HASH
+ when :readonly
+ false
+ when :where, :having
+ Relation::WhereClause.empty
+ when :from
+ Relation::FromClause.empty
+ when *Relation::MULTI_VALUE_METHODS
+ FROZEN_EMPTY_ARRAY
+ when *Relation::SINGLE_VALUE_METHODS
+ nil
+ else
+ raise ArgumentError, "unknown relation value #{name.inspect}"
+ end
+ end
end
end
diff --git a/activerecord/lib/active_record/relation/record_fetch_warning.rb b/activerecord/lib/active_record/relation/record_fetch_warning.rb
index dbd08811fa..31544c730e 100644
--- a/activerecord/lib/active_record/relation/record_fetch_warning.rb
+++ b/activerecord/lib/active_record/relation/record_fetch_warning.rb
@@ -2,15 +2,15 @@ module ActiveRecord
class Relation
module RecordFetchWarning
# When this module is prepended to ActiveRecord::Relation and
- # `config.active_record.warn_on_records_fetched_greater_than` is
+ # +config.active_record.warn_on_records_fetched_greater_than+ is
# set to an integer, if the number of records a query returns is
- # greater than the value of `warn_on_records_fetched_greater_than`,
+ # greater than the value of +warn_on_records_fetched_greater_than+,
# a warning is logged. This allows for the detection of queries that
# return a large number of records, which could cause memory bloat.
#
# In most cases, fetching large number of records can be performed
# efficiently using the ActiveRecord::Batches methods.
- # See active_record/lib/relation/batches.rb for more information.
+ # See ActiveRecord::Batches for more information.
def exec_queries
QueryRegistry.reset
diff --git a/activerecord/lib/active_record/relation/spawn_methods.rb b/activerecord/lib/active_record/relation/spawn_methods.rb
index d5c18a2a4a..190e339ea8 100644
--- a/activerecord/lib/active_record/relation/spawn_methods.rb
+++ b/activerecord/lib/active_record/relation/spawn_methods.rb
@@ -1,10 +1,9 @@
-require 'active_support/core_ext/hash/except'
-require 'active_support/core_ext/hash/slice'
-require 'active_record/relation/merger'
+require "active_support/core_ext/hash/except"
+require "active_support/core_ext/hash/slice"
+require "active_record/relation/merger"
module ActiveRecord
module SpawnMethods
-
# This is overridden by Associations::CollectionProxy
def spawn #:nodoc:
clone
diff --git a/activerecord/lib/active_record/relation/where_clause.rb b/activerecord/lib/active_record/relation/where_clause.rb
index 89396b518c..402f8acfd1 100644
--- a/activerecord/lib/active_record/relation/where_clause.rb
+++ b/activerecord/lib/active_record/relation/where_clause.rb
@@ -86,89 +86,89 @@ module ActiveRecord
protected
- attr_reader :predicates
+ attr_reader :predicates
- def referenced_columns
- @referenced_columns ||= begin
- equality_nodes = predicates.select { |n| equality_node?(n) }
- Set.new(equality_nodes, &:left)
+ def referenced_columns
+ @referenced_columns ||= begin
+ equality_nodes = predicates.select { |n| equality_node?(n) }
+ Set.new(equality_nodes, &:left)
+ end
end
- end
private
- def predicates_unreferenced_by(other)
- predicates.reject do |n|
- equality_node?(n) && other.referenced_columns.include?(n.left)
+ def predicates_unreferenced_by(other)
+ predicates.reject do |n|
+ equality_node?(n) && other.referenced_columns.include?(n.left)
+ end
end
- end
- def equality_node?(node)
- node.respond_to?(:operator) && node.operator == :==
- end
-
- def non_conflicting_binds(other)
- conflicts = referenced_columns & other.referenced_columns
- conflicts.map! { |node| node.name.to_s }
- binds.reject { |attr| conflicts.include?(attr.name) }
- end
+ def equality_node?(node)
+ node.respond_to?(:operator) && node.operator == :==
+ end
- def inverted_predicates
- predicates.map { |node| invert_predicate(node) }
- end
+ def non_conflicting_binds(other)
+ conflicts = referenced_columns & other.referenced_columns
+ conflicts.map! { |node| node.name.to_s }
+ binds.reject { |attr| conflicts.include?(attr.name) }
+ end
- def invert_predicate(node)
- case node
- when NilClass
- raise ArgumentError, 'Invalid argument for .where.not(), got nil.'
- when Arel::Nodes::In
- Arel::Nodes::NotIn.new(node.left, node.right)
- when Arel::Nodes::Equality
- Arel::Nodes::NotEqual.new(node.left, node.right)
- when String
- Arel::Nodes::Not.new(Arel::Nodes::SqlLiteral.new(node))
- else
- Arel::Nodes::Not.new(node)
+ def inverted_predicates
+ predicates.map { |node| invert_predicate(node) }
end
- end
- def predicates_except(columns)
- predicates.reject do |node|
+ def invert_predicate(node)
case node
- when Arel::Nodes::Between, Arel::Nodes::In, Arel::Nodes::NotIn, Arel::Nodes::Equality, Arel::Nodes::NotEqual, Arel::Nodes::LessThan, Arel::Nodes::LessThanOrEqual, Arel::Nodes::GreaterThan, Arel::Nodes::GreaterThanOrEqual
- subrelation = (node.left.kind_of?(Arel::Attributes::Attribute) ? node.left : node.right)
- columns.include?(subrelation.name.to_s)
+ when NilClass
+ raise ArgumentError, "Invalid argument for .where.not(), got nil."
+ when Arel::Nodes::In
+ Arel::Nodes::NotIn.new(node.left, node.right)
+ when Arel::Nodes::Equality
+ Arel::Nodes::NotEqual.new(node.left, node.right)
+ when String
+ Arel::Nodes::Not.new(Arel::Nodes::SqlLiteral.new(node))
+ else
+ Arel::Nodes::Not.new(node)
end
end
- end
- def binds_except(columns)
- binds.reject do |attr|
- columns.include?(attr.name)
+ def predicates_except(columns)
+ predicates.reject do |node|
+ case node
+ when Arel::Nodes::Between, Arel::Nodes::In, Arel::Nodes::NotIn, Arel::Nodes::Equality, Arel::Nodes::NotEqual, Arel::Nodes::LessThan, Arel::Nodes::LessThanOrEqual, Arel::Nodes::GreaterThan, Arel::Nodes::GreaterThanOrEqual
+ subrelation = (node.left.kind_of?(Arel::Attributes::Attribute) ? node.left : node.right)
+ columns.include?(subrelation.name.to_s)
+ end
+ end
end
- end
- def predicates_with_wrapped_sql_literals
- non_empty_predicates.map do |node|
- if Arel::Nodes::Equality === node
- node
- else
- wrap_sql_literal(node)
+ def binds_except(columns)
+ binds.reject do |attr|
+ columns.include?(attr.name)
end
end
- end
- ARRAY_WITH_EMPTY_STRING = ['']
- def non_empty_predicates
- predicates - ARRAY_WITH_EMPTY_STRING
- end
+ def predicates_with_wrapped_sql_literals
+ non_empty_predicates.map do |node|
+ if Arel::Nodes::Equality === node
+ node
+ else
+ wrap_sql_literal(node)
+ end
+ end
+ end
- def wrap_sql_literal(node)
- if ::String === node
- node = Arel.sql(node)
+ ARRAY_WITH_EMPTY_STRING = [""]
+ def non_empty_predicates
+ predicates - ARRAY_WITH_EMPTY_STRING
+ end
+
+ def wrap_sql_literal(node)
+ if ::String === node
+ node = Arel.sql(node)
+ end
+ Arel::Nodes::Grouping.new(node)
end
- Arel::Nodes::Grouping.new(node)
- end
end
end
end
diff --git a/activerecord/lib/active_record/relation/where_clause_factory.rb b/activerecord/lib/active_record/relation/where_clause_factory.rb
index dbf172a577..1e7deeffad 100644
--- a/activerecord/lib/active_record/relation/where_clause_factory.rb
+++ b/activerecord/lib/active_record/relation/where_clause_factory.rb
@@ -7,8 +7,6 @@ module ActiveRecord
end
def build(opts, other)
- binds = []
-
case opts
when String, Array
parts = [klass.send(:sanitize_sql, other.empty? ? opts : ([opts] + other))]
@@ -22,17 +20,16 @@ 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
- WhereClause.new(parts, binds)
+ WhereClause.new(parts, binds || [])
end
protected
- attr_reader :klass, :predicate_builder
+ attr_reader :klass, :predicate_builder
end
end
end