diff options
Diffstat (limited to 'activerecord/lib/active_record/relation')
23 files changed, 685 insertions, 660 deletions
diff --git a/activerecord/lib/active_record/relation/batches.rb b/activerecord/lib/active_record/relation/batches.rb index 76031515fd..9c579843b1 100644 --- a/activerecord/lib/active_record/relation/batches.rb +++ b/activerecord/lib/active_record/relation/batches.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + require "active_record/relation/batches/batch_enumerator" module ActiveRecord @@ -30,14 +32,14 @@ module ActiveRecord # end # # ==== Options - # * <tt>:batch_size</tt> - Specifies the size of the batch. Default to 1000. + # * <tt>:batch_size</tt> - Specifies the size of the batch. Defaults to 1000. # * <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 - # an order is present in the relation. + # 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. + # size: it can be less than, equal to, or greater than the limit. # # The options +start+ and +finish+ are especially useful if you want # multiple workers dealing with the same processing queue. You can make @@ -45,7 +47,12 @@ module ActiveRecord # handle from 10000 and beyond by setting the +:start+ and +:finish+ # option on each worker. # - # # Let's process from record 10_000 on. + # # In worker 1, let's process until 9999 records. + # Person.find_each(finish: 9_999) do |person| + # person.party_all_night! + # end + # + # # In worker 2, let's process from record 10_000 and onwards. # Person.find_each(start: 10_000) do |person| # person.party_all_night! # end @@ -89,14 +96,14 @@ module ActiveRecord # To be yielded each record one by one, use #find_each instead. # # ==== Options - # * <tt>:batch_size</tt> - Specifies the size of the batch. Default to 1000. + # * <tt>:batch_size</tt> - Specifies the size of the batch. Defaults to 1000. # * <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 - # an order is present in the relation. + # 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. + # size: it can be less than, equal to, or greater than the limit. # # The options +start+ and +finish+ are especially useful if you want # multiple workers dealing with the same processing queue. You can make @@ -140,9 +147,9 @@ module ActiveRecord # If you do not provide a block to #in_batches, it will return a # BatchEnumerator which is enumerable. # - # Person.in_batches.with_index do |relation, batch_index| + # Person.in_batches.each_with_index do |relation, batch_index| # puts "Processing relation ##{batch_index}" - # relation.each { |relation| relation.delete_all } + # relation.delete_all # end # # Examples of calling methods on the returned BatchEnumerator object: @@ -152,12 +159,12 @@ module ActiveRecord # Person.in_batches.each_record(&:party_all_night!) # # ==== Options - # * <tt>:of</tt> - Specifies the size of the batch. Default to 1000. - # * <tt>:load</tt> - Specifies if the relation should be loaded. Default to false. + # * <tt>:of</tt> - Specifies the size of the batch. Defaults to 1000. + # * <tt>:load</tt> - Specifies if the relation should be loaded. Defaults to false. # * <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 - # an order is present in the relation. + # 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. @@ -186,7 +193,7 @@ module ActiveRecord # # NOTE: It's not possible to set the order. That is automatically set to # ascending on the primary key ("id ASC") to make the batch ordering - # consistent. Therefore the primary key must be orderable, e.g an integer + # consistent. Therefore the primary key must be orderable, e.g. an integer # or a string. # # NOTE: By its nature, batch processing is subject to race conditions if @@ -209,6 +216,7 @@ module ActiveRecord relation = relation.reorder(batch_order).limit(batch_limit) relation = apply_limits(relation, start, finish) + relation.skip_query_cache! # Retaining the results in the query cache would undermine the point of batching batch_relation = relation loop do @@ -243,20 +251,30 @@ module ActiveRecord end end - batch_relation = relation.where(arel_attribute(primary_key).gt(primary_key_offset)) + batch_relation = relation.where( + bind_attribute(primary_key, primary_key_offset) { |attr, bind| attr.gt(bind) } + ) 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 = apply_start_limit(relation, start) if start + relation = apply_finish_limit(relation, finish) if finish relation end + def apply_start_limit(relation, start) + relation.where(bind_attribute(primary_key, start) { |attr, bind| attr.gteq(bind) }) + end + + def apply_finish_limit(relation, finish) + relation.where(bind_attribute(primary_key, finish) { |attr, bind| attr.lteq(bind) }) + end + def batch_order - "#{quoted_table_name}.#{quoted_primary_key} ASC" + arel_attribute(primary_key).asc end def act_on_ignored_order(error_on_ignore) diff --git a/activerecord/lib/active_record/relation/batches/batch_enumerator.rb b/activerecord/lib/active_record/relation/batches/batch_enumerator.rb index 3555779ec2..49697da3bf 100644 --- a/activerecord/lib/active_record/relation/batches/batch_enumerator.rb +++ b/activerecord/lib/active_record/relation/batches/batch_enumerator.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + module ActiveRecord module Batches class BatchEnumerator diff --git a/activerecord/lib/active_record/relation/calculations.rb b/activerecord/lib/active_record/relation/calculations.rb index f4cdaf3948..0fa5ba2e50 100644 --- a/activerecord/lib/active_record/relation/calculations.rb +++ b/activerecord/lib/active_record/relation/calculations.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + module ActiveRecord module Calculations # Count the records. @@ -38,10 +40,16 @@ module ActiveRecord # between databases. In invalid cases, an error from the database is thrown. def count(column_name = nil) if block_given? - to_a.count { |*block_args| yield(*block_args) } - else - calculate(:count, column_name) + unless column_name.nil? + ActiveSupport::Deprecation.warn \ + "When `count' is called with a block, it ignores other arguments. " \ + "This behavior is now deprecated and will result in an ArgumentError in Rails 6.0." + end + + return super() end + + calculate(:count, column_name) end # Calculates the average value on a given column. Returns +nil+ if there's @@ -75,8 +83,17 @@ module ActiveRecord # #calculate for examples with options. # # Person.sum(:age) # => 4562 - def sum(column_name = nil, &block) - return super(&block) if block_given? + def sum(column_name = nil) + if block_given? + unless column_name.nil? + ActiveSupport::Deprecation.warn \ + "When `sum' is called with a block, it ignores other arguments. " \ + "This behavior is now deprecated and will result in an ArgumentError in Rails 6.0." + end + + return super() + end + calculate(:sum, column_name) end @@ -113,8 +130,15 @@ module ActiveRecord # end def calculate(operation, column_name) if has_include?(column_name) - relation = construct_relation_for_association_calculations - relation = relation.distinct if operation.to_s.downcase == "count" + relation = apply_join_dependency + + if operation.to_s.downcase == "count" + relation.distinct! + # PostgreSQL: ORDER BY expressions must appear in SELECT list when using DISTINCT + if (column_name == :all || column_name.nil?) && select_values.empty? + relation.order_values = [] + end + end relation.calculate(operation, column_name) else @@ -163,17 +187,37 @@ module ActiveRecord end if has_include?(column_names.first) - construct_relation_for_association_calculations.pluck(*column_names) + relation = apply_join_dependency + relation.pluck(*column_names) else + disallow_raw_sql!(column_names) relation = spawn relation.select_values = column_names.map { |cn| @klass.has_attribute?(cn) || @klass.attribute_alias?(cn) ? arel_attribute(cn) : cn } - result = klass.connection.select_all(relation.arel, nil, bound_attributes) + result = skip_query_cache_if_necessary { klass.connection.select_all(relation.arel, nil) } result.cast_values(klass.attribute_types) end end + # Pick the value(s) from the named column(s) in the current relation. + # This is short-hand for <tt>relation.limit(1).pluck(*column_names).first</tt>, and is primarily useful + # when you have a relation that's already narrowed down to a single row. + # + # Just like #pluck, #pick will only load the actual value, not the entire record object, so it's also + # more efficient. The value is, again like with pluck, typecast by the column type. + # + # Person.where(id: 1).pick(:name) + # # SELECT people.name FROM people WHERE id = 1 LIMIT 1 + # # => 'David' + # + # Person.where(id: 1).pick(:name, :email_address) + # # SELECT people.name, people.email_address FROM people WHERE id = 1 LIMIT 1 + # # => [ 'David', 'david@loudthinking.com' ] + def pick(*column_names) + limit(1).pluck(*column_names).first + end + # Pluck all the ID's for the relation using the table's primary key # # Person.ids # SELECT people.id FROM people @@ -197,8 +241,13 @@ module ActiveRecord 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 + if column_name == :all + if distinct && (group_values.any? || select_values.empty? && order_values.empty?) + column_name = primary_key + end + elsif /\s*DISTINCT[\s(]+/i.match?(column_name.to_s) + distinct = nil + end end if group_values.any? @@ -211,7 +260,7 @@ module ActiveRecord def aggregate_column(column_name) return column_name if Arel::Expressions === column_name - if @klass.has_attribute?(column_name.to_s) || @klass.attribute_alias?(column_name.to_s) + if @klass.has_attribute?(column_name) || @klass.attribute_alias?(column_name) @klass.arel_attribute(column_name) else Arel.sql(column_name == :all ? "*" : column_name.to_s) @@ -225,7 +274,7 @@ module ActiveRecord def execute_simple_calculation(operation, column_name, distinct) #:nodoc: column_alias = column_name - if operation == "count" && (limit_value || offset_value) + if operation == "count" && (column_name == :all && distinct || has_limit_or_offset?) # Shortcut when limit is zero. return 0 if limit_value == 0 @@ -237,6 +286,9 @@ module ActiveRecord column = aggregate_column(column_name) select_value = operation_over_aggregate_column(column, operation, distinct) + if operation == "sum" && distinct + select_value.distinct = true + end column_alias = select_value.alias column_alias ||= @klass.connection.column_name_for_operation(operation, select_value) @@ -245,7 +297,7 @@ module ActiveRecord query_builder = relation.arel end - result = @klass.connection.select_all(query_builder, nil, bound_attributes) + result = skip_query_cache_if_necessary { @klass.connection.select_all(query_builder, nil) } row = result.first value = row && row.values.first type = result.column_types.fetch(column_alias) do @@ -296,7 +348,7 @@ module ActiveRecord relation.group_values = group_fields relation.select_values = select_values - calculated_data = @klass.connection.select_all(relation, nil, relation.bound_attributes) + calculated_data = skip_query_cache_if_necessary { @klass.connection.select_all(relation.arel, nil) } if association key_ids = calculated_data.collect { |row| row[group_aliases.first] } @@ -364,16 +416,17 @@ module ActiveRecord end def build_count_subquery(relation, column_name, distinct) - column_alias = Arel.sql("count_column") - subquery_alias = Arel.sql("subquery_for_count") + if column_name == :all + relation.select_values = [ Arel.sql(FinderMethods::ONE_AS_ONE) ] unless distinct + else + column_alias = Arel.sql("count_column") + relation.select_values = [ aggregate_column(column_name).as(column_alias) ] + end - aliased_column = aggregate_column(column_name == :all ? 1 : column_name).as(column_alias) - relation.select_values = [aliased_column] - subquery = relation.arel.as(subquery_alias) + subquery = relation.arel.as(Arel.sql("subquery_for_count")) + select_value = operation_over_aggregate_column(column_alias || Arel.star, "count", false) - sm = Arel::SelectManager.new relation.engine - select_value = operation_over_aggregate_column(column_alias, "count", distinct) - sm.project(select_value).from(subquery) + Arel::SelectManager.new(subquery).project(select_value) end end end diff --git a/activerecord/lib/active_record/relation/delegation.rb b/activerecord/lib/active_record/relation/delegation.rb index d3ba724507..488f71cdde 100644 --- a/activerecord/lib/active_record/relation/delegation.rb +++ b/activerecord/lib/active_record/relation/delegation.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + module ActiveRecord module Delegation # :nodoc: module DelegateCache # :nodoc: @@ -36,13 +38,12 @@ module ActiveRecord # may vary depending on the klass of a relation, so we create a subclass of Relation # for each different klass, and the delegations are compiled into that subclass only. - delegate :to_xml, :encode_with, :length, :collect, :map, :each, :all?, :include?, :to_ary, :join, - :[], :&, :|, :+, :-, :sample, :reverse, :compact, :in_groups, :in_groups_of, - :to_sentence, :to_formatted_s, - :shuffle, :split, :index, to: :records + delegate :to_xml, :encode_with, :length, :each, :join, + :[], :&, :|, :+, :-, :sample, :reverse, :rotate, :compact, :in_groups, :in_groups_of, + :to_sentence, :to_formatted_s, :as_json, + :shuffle, :split, :slice, :index, :rindex, to: :records - delegate :table_name, :quoted_table_name, :primary_key, :quoted_primary_key, - :connection, :columns_hash, to: :klass + delegate :primary_key, :connection, to: :klass module ClassSpecificRelation # :nodoc: extend ActiveSupport::Concern @@ -73,13 +74,6 @@ module ActiveRecord end end end - - def delegate(method, opts = {}) - @delegation_mutex.synchronize do - return if method_defined?(method) - super - end - end end private @@ -88,8 +82,14 @@ module ActiveRecord if @klass.respond_to?(method) self.class.delegate_to_scoped_klass(method) scoping { @klass.public_send(method, *args, &block) } + elsif @delegate_to_klass && @klass.respond_to?(method, true) + ActiveSupport::Deprecation.warn \ + "Delegating missing #{method} method to #{@klass}. " \ + "Accessibility of private/protected class methods in :scope is deprecated and will be removed in Rails 6.0." + @klass.send(method, *args, &block) elsif arel.respond_to?(method) - self.class.delegate method, to: :arel + ActiveSupport::Deprecation.warn \ + "Delegating #{method} to arel is deprecated and will be removed in Rails 6.0." arel.public_send(method, *args, &block) else super @@ -109,21 +109,9 @@ module ActiveRecord end end - def respond_to_missing?(method, include_private = false) - super || @klass.respond_to?(method, include_private) || - arel.respond_to?(method, include_private) - end - private - - 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 + def respond_to_missing?(method, _) + super || @klass.respond_to?(method) || arel.respond_to?(method) end end end diff --git a/activerecord/lib/active_record/relation/finder_methods.rb b/activerecord/lib/active_record/relation/finder_methods.rb index 5d24f5f5ca..0fabfe5518 100644 --- a/activerecord/lib/active_record/relation/finder_methods.rb +++ b/activerecord/lib/active_record/relation/finder_methods.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + require "active_support/core_ext/string/filters" module ActiveRecord @@ -16,9 +18,10 @@ module ActiveRecord # Person.find([1]) # returns an array for the object with ID = 1 # Person.where("administrator = 1").order("created_on DESC").find(1) # - # NOTE: The returned records may not be in the same order as the ids you - # provide since database rows are unordered. You will need to provide an explicit QueryMethods#order - # option if you want the results to be sorted. + # NOTE: The returned records are in the same order as the ids you provide. + # If you want the results to be sorted by database, you can use ActiveRecord::QueryMethods#where + # method and provide an explicit ActiveRecord::QueryMethods#order option. + # But ActiveRecord::QueryMethods#where method doesn't raise ActiveRecord::RecordNotFound. # # ==== Find with lock # @@ -86,7 +89,7 @@ module ActiveRecord where(arg, *args).take! rescue ::RangeError raise RecordNotFound.new("Couldn't find #{@klass.name} with an out of range value", - @klass.name) + @klass.name, @klass.primary_key) end # Gives a record (or N records if a parameter is supplied) without any implied @@ -145,10 +148,9 @@ module ActiveRecord # # [#<Person id:4>, #<Person id:3>, #<Person id:2>] def last(limit = nil) - return find_last(limit) if loaded? || limit_value + return find_last(limit) if loaded? || has_limit_or_offset? - result = limit(limit) - result.order!(arel_attribute(primary_key)) if order_values.empty? && primary_key + result = ordered_relation.limit(limit) result = result.reverse_order! limit ? result.reverse : result.first @@ -283,7 +285,7 @@ module ActiveRecord # * Hash - Finds the record that matches these +find+-style conditions # (such as <tt>{name: 'David'}</tt>). # * +false+ - Returns always +false+. - # * No args - Returns +false+ if the table is empty, +true+ otherwise. + # * No args - Returns +false+ if the relation is empty, +true+ otherwise. # # For more information about specifying conditions as a hash or array, # see the Conditions section in the introduction to ActiveRecord::Base. @@ -299,6 +301,7 @@ module ActiveRecord # Person.exists?(name: 'David') # Person.exists?(false) # Person.exists? + # Person.where(name: 'Spartacus', rating: 4).exists? def exists?(conditions = :none) if Base === conditions raise ArgumentError, <<-MSG.squish @@ -307,23 +310,16 @@ module ActiveRecord MSG end - return false if !conditions - - relation = apply_join_dependency(self, construct_join_dependency(eager_loading: false)) - return false if ActiveRecord::NullRelation === relation + return false if !conditions || limit_value == 0 - relation = relation.except(:select, :distinct).select(ONE_AS_ONE).limit(1) - - case conditions - when Array, Hash - relation = relation.where(conditions) - else - unless conditions == :none - relation = relation.where(primary_key => conditions) - end + if eager_loading? + relation = apply_join_dependency(eager_loading: false) + return relation.exists?(conditions) end - connection.select_value(relation, "#{name} Exists", relation.bound_attributes) ? true : false + relation = construct_relation_for_exists(conditions) + + skip_query_cache_if_necessary { connection.select_value(relation.arel, "#{name} Exists") } ? true : false rescue ::RangeError false end @@ -336,23 +332,23 @@ 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 = nil, result_size = nil, expected_size = nil, key = primary_key) # :nodoc: - conditions = arel.where_sql(@klass.arel_engine) + def raise_record_not_found_exception!(ids = nil, result_size = nil, expected_size = nil, key = primary_key, not_found_ids = nil) # :nodoc: + conditions = arel.where_sql(@klass) conditions = " [#{conditions}]" if conditions name = @klass.name if ids.nil? - error = "Couldn't find #{name}" + error = "Couldn't find #{name}".dup error << " with#{conditions}" if conditions - raise RecordNotFound.new(error, name) + raise RecordNotFound.new(error, name, key) elsif Array(ids).size == 1 error = "Couldn't find #{name} with '#{key}'=#{ids}#{conditions}" raise RecordNotFound.new(error, name, key, ids) else - error = "Couldn't find all #{name.pluralize} with '#{key}': " - error << "(#{ids.join(", ")})#{conditions} (found #{result_size} results, but was looking for #{expected_size})" - - raise RecordNotFound.new(error, name, primary_key, ids) + error = "Couldn't find all #{name.pluralize} with '#{key}': ".dup + error << "(#{ids.join(", ")})#{conditions} (found #{result_size} results, but was looking for #{expected_size})." + error << " Couldn't find #{name.pluralize(not_found_ids.size)} with #{key.to_s.pluralize(not_found_ids.size)} #{not_found_ids.join(', ')}." if not_found_ids + raise RecordNotFound.new(error, name, key, ids) end end @@ -362,67 +358,53 @@ module ActiveRecord 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) + def construct_relation_for_exists(conditions) + relation = except(:select, :distinct, :order)._select!(ONE_AS_ONE).limit!(1) - if block_given? - yield relation + case conditions + when Array, Hash + relation.where!(conditions) else - if ActiveRecord::NullRelation === relation - [] - else - arel = relation.arel - rows = connection.select_all(arel, "SQL", relation.bound_attributes) - join_dependency.instantiate(rows, aliases) - end + relation.where!(primary_key => conditions) unless conditions == :none 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) + relation end - def construct_relation_for_association_calculations - apply_join_dependency(self, construct_join_dependency(joins_values)) + def construct_join_dependency(associations) + ActiveRecord::Associations::JoinDependency.new( + klass, table, associations + ) end - def apply_join_dependency(relation, join_dependency) - relation = relation.except(:includes, :eager_load, :preload) - relation = relation.joins join_dependency + def apply_join_dependency(eager_loading: group_values.empty?) + join_dependency = construct_join_dependency(eager_load_values + includes_values) + relation = except(:includes, :eager_load, :preload).joins!(join_dependency) - if using_limitable_reflections?(join_dependency.reflections) - relation - else - if relation.limit_value + if eager_loading && !using_limitable_reflections?(join_dependency.reflections) + if has_limit_or_offset? limited_ids = limited_ids_for(relation) limited_ids.empty? ? relation.none! : relation.where!(primary_key => limited_ids) end - relation.except(:limit, :offset) + relation.limit_value = relation.offset_value = nil + end + + if block_given? + yield relation, join_dependency + else + relation end end def limited_ids_for(relation) values = @klass.connection.columns_for_distinct( - "#{quoted_table_name}.#{quoted_primary_key}", relation.order_values) + connection.visitor.compile(arel_attribute(primary_key)), + relation.order_values + ) relation = relation.except(:select).select(values).distinct! - arel = relation.arel - id_rows = @klass.connection.select_all(arel, "SQL", relation.bound_attributes) + id_rows = skip_query_cache_if_necessary { @klass.connection.select_all(relation.arel, "SQL") } id_rows.map { |row| row[primary_key] } end @@ -438,9 +420,12 @@ module ActiveRecord ids = ids.flatten.compact.uniq + model_name = @klass.name + case ids.size when 0 - raise RecordNotFound, "Couldn't find #{@klass.name} without an ID" + error_message = "Couldn't find #{model_name} without an ID" + raise RecordNotFound.new(error_message, model_name, primary_key) when 1 result = find_one(ids.first) expects_array ? [ result ] : result @@ -448,7 +433,8 @@ module ActiveRecord find_some(ids) end rescue ::RangeError - raise RecordNotFound, "Couldn't find #{@klass.name} with an out of range ID" + error_message = "Couldn't find #{model_name} with an out of range ID" + raise RecordNotFound.new(error_message, model_name, primary_key, ids) end def find_one(id) @@ -530,13 +516,13 @@ module ActiveRecord if loaded? records[index, limit] || [] else - relation = if order_values.empty? && primary_key - order(arel_attribute(primary_key).asc) - else - self + relation = ordered_relation + + if limit_value + limit = [limit_value - index, limit].min end - if limit_value.nil? || index < limit_value + if limit > 0 relation = relation.offset(offset_index + index) unless index.zero? relation.limit(limit).to_a else @@ -549,23 +535,26 @@ module ActiveRecord if loaded? records[-index] else - relation = if order_values.empty? && primary_key - order(arel_attribute(primary_key).asc) + relation = ordered_relation + + if equal?(relation) || has_limit_or_offset? + relation.records[-index] else - self + relation.last(index)[-index] 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 def find_last(limit) limit ? records.last(limit) : records.last end + + def ordered_relation + if order_values.empty? && primary_key + order(arel_attribute(primary_key).asc) + else + self + end + end end end diff --git a/activerecord/lib/active_record/relation/from_clause.rb b/activerecord/lib/active_record/relation/from_clause.rb index 8945cb0cc5..c53a682aee 100644 --- a/activerecord/lib/active_record/relation/from_clause.rb +++ b/activerecord/lib/active_record/relation/from_clause.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + module ActiveRecord class Relation class FromClause # :nodoc: @@ -8,14 +10,6 @@ module ActiveRecord @name = name end - def binds - if value.is_a?(Relation) - value.bound_attributes - else - [] - end - end - def merge(other) self end diff --git a/activerecord/lib/active_record/relation/merger.rb b/activerecord/lib/active_record/relation/merger.rb index 5dac00724a..07b16a0740 100644 --- a/activerecord/lib/active_record/relation/merger.rb +++ b/activerecord/lib/active_record/relation/merger.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + require "active_support/core_ext/hash/keys" module ActiveRecord @@ -21,7 +23,11 @@ module ActiveRecord # build a relation to merge in rather than directly merging # the values. def other - other = Relation.create(relation.klass, relation.table, relation.predicate_builder) + other = Relation.create( + relation.klass, + table: relation.table, + predicate_builder: relation.predicate_builder + ) hash.each { |k, v| if k == :joins if Hash === v @@ -50,7 +56,7 @@ module ActiveRecord NORMAL_VALUES = Relation::VALUE_METHODS - Relation::CLAUSE_METHODS - - [:includes, :preload, :joins, :order, :reverse_order, :lock, :create_with, :reordering] # :nodoc: + [:includes, :preload, :joins, :left_outer_joins, :order, :reverse_order, :lock, :create_with, :reordering] # :nodoc: def normal_values NORMAL_VALUES @@ -77,6 +83,7 @@ module ActiveRecord merge_clauses merge_preloads merge_joins + merge_outer_joins relation end @@ -110,41 +117,53 @@ module ActiveRecord if other.klass == relation.klass relation.joins!(*other.joins_values) else - joins_dependency, rest = other.joins_values.partition do |join| + joins_dependency = other.joins_values.map do |join| case join when Hash, Symbol, Array - true + other.send(:construct_join_dependency, join) else - false + join end end - join_dependency = ActiveRecord::Associations::JoinDependency.new(other.klass, - joins_dependency, - []) - relation.joins! rest + relation.joins!(*joins_dependency) + end + end - @relation = relation.joins join_dependency + def merge_outer_joins + return if other.left_outer_joins_values.blank? + + if other.klass == relation.klass + relation.left_outer_joins!(*other.left_outer_joins_values) + else + joins_dependency = other.left_outer_joins_values.map do |join| + case join + when Hash, Symbol, Array + other.send(:construct_join_dependency, join) + else + join + end + end + + relation.left_outer_joins!(*joins_dependency) end 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 + relation.reorder!(*other.order_values) + elsif other.order_values.any? # merge in order_values from relation - relation.order! other.order_values + relation.order!(*other.order_values) end - relation.extend(*other.extending_values) unless other.extending_values.blank? + extensions = other.extensions - relation.extensions + relation.extending!(*extensions) if extensions.any? end def merge_single_values - if relation.from_clause.empty? - relation.from_clause = other.from_clause - end - relation.lock_value ||= other.lock_value + relation.lock_value ||= other.lock_value if other.lock_value unless other.create_with_value.blank? relation.create_with_value = (relation.create_with_value || {}).merge(other.create_with_value) @@ -152,11 +171,15 @@ module ActiveRecord end 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)) + if relation.from_clause.empty? && !other.from_clause.empty? + relation.from_clause = other.from_clause end + + where_clause = relation.where_clause.merge(other.where_clause) + relation.where_clause = where_clause unless where_clause.empty? + + having_clause = relation.having_clause.merge(other.having_clause) + relation.having_clause = having_clause unless having_clause.empty? end end end diff --git a/activerecord/lib/active_record/relation/predicate_builder.rb b/activerecord/lib/active_record/relation/predicate_builder.rb index 18ae10a652..f734cd0ad8 100644 --- a/activerecord/lib/active_record/relation/predicate_builder.rb +++ b/activerecord/lib/active_record/relation/predicate_builder.rb @@ -1,27 +1,19 @@ +# frozen_string_literal: true + 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/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 def initialize(table) @table = table @handlers = [] - register_handler(BasicObject, BasicObjectHandler.new) + register_handler(BasicObject, BasicObjectHandler.new(self)) register_handler(Base, BaseHandler.new(self)) - register_handler(Range, RangeHandler.new) - register_handler(RangeHandler::RangeWithBinds, RangeHandler.new) + register_handler(Range, RangeHandler.new(self)) register_handler(Relation, RelationHandler.new) register_handler(Array, ArrayHandler.new(self)) - register_handler(AssociationQueryValue, AssociationQueryHandler.new(self)) - register_handler(PolymorphicArrayValue, PolymorphicArrayHandler.new(self)) + register_handler(Set, ArrayHandler.new(self)) end def build_from_hash(attributes) @@ -29,11 +21,6 @@ module ActiveRecord expand_from_hash(attributes) end - def create_binds(attributes) - attributes = convert_dot_notation_to_hash(attributes) - create_binds_for_hash(attributes) - end - def self.references(attributes) attributes.map do |key, value| if value.is_a?(Hash) @@ -61,74 +48,66 @@ module ActiveRecord end def build(attribute, value) - handler_for(value).call(attribute, value) + if table.type(attribute.name).force_equality?(value) + bind = build_bind_attribute(attribute.name, value) + attribute.eq(bind) + else + handler_for(value).call(attribute, value) + end end - # TODO Change this to private once we've dropped Ruby 2.2 support. - # Workaround for Ruby 2.2 "private attribute?" warning. - protected - - attr_reader :table + def build_bind_attribute(column_name, value) + attr = Relation::QueryAttribute.new(column_name.to_s, value, table.type(column_name)) + Arel::Nodes::BindParam.new(attr) + end + protected def expand_from_hash(attributes) return ["1=0"] if attributes.empty? 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 + elsif table.associated_with?(key) + # 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) + associated_table = table.associated_table(key) + if associated_table.polymorphic_association? + case value.is_a?(Array) ? value.first : value + when Base, Relation + value = [value] unless value.is_a?(Array) + klass = PolymorphicArrayValue + end + end - 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 + klass ||= AssociationQueryValue + queries = klass.new(associated_table, value).queries.map do |query| + expand_from_hash(query).reduce(&:and) end - unless last.respond_to?(:infinite?) && last.infinite? - binds << build_bind_param(column_name, last) - last = Arel::Nodes::BindParam.new + queries.reduce(&:or) + elsif table.aggregated_with?(key) + mapping = table.reflect_on_aggregation(key).mapping + queries = Array.wrap(value).map do |object| + mapping.map do |field_attr, aggregate_attr| + if mapping.size == 1 && !object.respond_to?(aggregate_attr) + build(table.arel_attribute(field_attr), object) + else + build(table.arel_attribute(field_attr), object.send(aggregate_attr)) + end + end.reduce(&:and) end - - result[column_name] = RangeHandler::RangeWithBinds.new(first, last, value.exclude_end?) + queries.reduce(&:or) else - if can_be_bound?(column_name, value) - result[column_name] = Arel::Nodes::BindParam.new - binds << build_bind_param(column_name, value) - end - end - - # 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) + build(table.arel_attribute(key), value) end end - - [result, binds] end private + attr_reader :table def associated_predicate_builder(association_name) self.class.new(table.associated_table(association_name)) @@ -153,19 +132,14 @@ module ActiveRecord def handler_for(object) @handlers.detect { |klass, _| klass === object }.last 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 + +require "active_record/relation/predicate_builder/array_handler" +require "active_record/relation/predicate_builder/base_handler" +require "active_record/relation/predicate_builder/basic_object_handler" +require "active_record/relation/predicate_builder/range_handler" +require "active_record/relation/predicate_builder/relation_handler" + +require "active_record/relation/predicate_builder/association_query_value" +require "active_record/relation/predicate_builder/polymorphic_array_value" 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 88b6c37d43..e5191fa38a 100644 --- a/activerecord/lib/active_record/relation/predicate_builder/array_handler.rb +++ b/activerecord/lib/active_record/relation/predicate_builder/array_handler.rb @@ -1,3 +1,7 @@ +# frozen_string_literal: true + +require "active_support/core_ext/array/extract" + module ActiveRecord class PredicateBuilder class ArrayHandler # :nodoc: @@ -6,18 +10,21 @@ module ActiveRecord end def call(attribute, value) - values = value.map { |x| x.is_a?(Base) ? x.id : x } - nils, values = values.partition(&:nil?) + return attribute.in([]) if value.empty? - return attribute.in([]) if values.empty? && nils.empty? - - ranges, values = values.partition { |v| v.is_a?(Range) } + values = value.map { |x| x.is_a?(Base) ? x.id : x } + nils = values.extract!(&:nil?) + ranges = values.extract! { |v| v.is_a?(Range) } values_predicate = case values.length when 0 then NullPredicate when 1 then predicate_builder.build(attribute, values.first) - else attribute.in(values) + else + bind_values = values.map do |v| + predicate_builder.build_bind_attribute(attribute.name, v) + end + attribute.in(bind_values) end unless nils.empty? @@ -26,13 +33,10 @@ module ActiveRecord array_predicates = ranges.map { |range| predicate_builder.build(attribute, range) } array_predicates.unshift(values_predicate) - array_predicates.inject { |composite, predicate| composite.or(predicate) } + array_predicates.inject(&:or) end - # TODO Change this to private once we've dropped Ruby 2.2 support. - # Workaround for Ruby 2.2 "private attribute?" warning. - protected - + private attr_reader :predicate_builder module NullPredicate # :nodoc: 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 deleted file mode 100644 index 29860ec677..0000000000 --- a/activerecord/lib/active_record/relation/predicate_builder/association_query_handler.rb +++ /dev/null @@ -1,88 +0,0 @@ -module ActiveRecord - class PredicateBuilder - class AssociationQueryHandler # :nodoc: - def self.value_for(table, column, value) - associated_table = table.associated_table(column) - klass = if associated_table.polymorphic_association? && ::Array === value && value.first.is_a?(Base) - PolymorphicArrayValue - else - AssociationQueryValue - end - - klass.new(associated_table, value) - end - - def initialize(predicate_builder) - @predicate_builder = predicate_builder - end - - def call(attribute, value) - queries = {} - - table = value.associated_table - if value.base_class - queries[table.association_foreign_type.to_s] = value.base_class.name - end - - queries[table.association_foreign_key.to_s] = value.ids - predicate_builder.build_from_hash(queries) - end - - # TODO Change this to private once we've dropped Ruby 2.2 support. - # Workaround for Ruby 2.2 "private attribute?" warning. - protected - - attr_reader :predicate_builder - end - - class AssociationQueryValue # :nodoc: - attr_reader :associated_table, :value - - def initialize(associated_table, value) - @associated_table = associated_table - @value = value - end - - def ids - case value - when Relation - value.select(primary_key) - when Array - value.map { |v| convert_to_id(v) } - else - convert_to_id(value) - end - end - - def base_class - if associated_table.polymorphic_association? - @base_class ||= polymorphic_base_class_from_value - end - end - - private - - 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 Base - value.class.base_class - end - end - - def convert_to_id(value) - case value - when Base - value._read_attribute(primary_key) - else - value - end - end - end - end -end diff --git a/activerecord/lib/active_record/relation/predicate_builder/association_query_value.rb b/activerecord/lib/active_record/relation/predicate_builder/association_query_value.rb new file mode 100644 index 0000000000..88cd71cf69 --- /dev/null +++ b/activerecord/lib/active_record/relation/predicate_builder/association_query_value.rb @@ -0,0 +1,43 @@ +# frozen_string_literal: true + +module ActiveRecord + class PredicateBuilder + class AssociationQueryValue # :nodoc: + def initialize(associated_table, value) + @associated_table = associated_table + @value = value + end + + def queries + [associated_table.association_join_foreign_key.to_s => ids] + end + + private + attr_reader :associated_table, :value + + def ids + case value + when Relation + value.select_values.empty? ? value.select(primary_key) : value + when Array + value.map { |v| convert_to_id(v) } + else + convert_to_id(value) + end + end + + def primary_key + associated_table.association_join_primary_key + end + + def convert_to_id(value) + case value + when Base + value._read_attribute(primary_key) + else + value + 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 3bb1037885..10c5c1a66a 100644 --- a/activerecord/lib/active_record/relation/predicate_builder/base_handler.rb +++ b/activerecord/lib/active_record/relation/predicate_builder/base_handler.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + module ActiveRecord class PredicateBuilder class BaseHandler # :nodoc: @@ -9,10 +11,7 @@ module ActiveRecord predicate_builder.build(attribute, value.id) end - # TODO Change this to private once we've dropped Ruby 2.2 support. - # Workaround for Ruby 2.2 "private attribute?" warning. - protected - + private attr_reader :predicate_builder 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 79cde00303..e8c9f60860 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,9 +1,19 @@ +# frozen_string_literal: true + module ActiveRecord class PredicateBuilder class BasicObjectHandler # :nodoc: + def initialize(predicate_builder) + @predicate_builder = predicate_builder + end + def call(attribute, value) - attribute.eq(value) + bind = predicate_builder.build_bind_attribute(attribute.name, value) + attribute.eq(bind) end + + private + attr_reader :predicate_builder 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 deleted file mode 100644 index 335124c952..0000000000 --- a/activerecord/lib/active_record/relation/predicate_builder/polymorphic_array_handler.rb +++ /dev/null @@ -1,59 +0,0 @@ -module ActiveRecord - class PredicateBuilder - class PolymorphicArrayHandler # :nodoc: - def initialize(predicate_builder) - @predicate_builder = predicate_builder - end - - def call(attribute, value) - table = value.associated_table - queries = value.type_to_ids_mapping.map do |type, ids| - { table.association_foreign_type.to_s => type, table.association_foreign_key.to_s => ids } - end - - predicates = queries.map { |query| predicate_builder.build_from_hash(query) } - - if predicates.size > 1 - type_and_ids_predicates = predicates.map { |type_predicate, id_predicate| Arel::Nodes::Grouping.new(type_predicate.and(id_predicate)) } - type_and_ids_predicates.inject(&:or) - else - predicates.first - end - end - - # TODO Change this to private once we've dropped Ruby 2.2 support. - # Workaround for Ruby 2.2 "private attribute?" warning. - protected - - attr_reader :predicate_builder - end - - class PolymorphicArrayValue # :nodoc: - attr_reader :associated_table, :values - - def initialize(associated_table, values) - @associated_table = associated_table - @values = values - end - - def type_to_ids_mapping - default_hash = Hash.new { |hsh, key| hsh[key] = [] } - values.each_with_object(default_hash) { |value, hash| hash[base_class(value).name] << convert_to_id(value) } - end - - private - - def primary_key(value) - associated_table.association_primary_key(base_class(value)) - end - - def base_class(value) - value.class.base_class - end - - def convert_to_id(value) - value._read_attribute(primary_key(value)) - end - end - end -end diff --git a/activerecord/lib/active_record/relation/predicate_builder/polymorphic_array_value.rb b/activerecord/lib/active_record/relation/predicate_builder/polymorphic_array_value.rb new file mode 100644 index 0000000000..aae04d9348 --- /dev/null +++ b/activerecord/lib/active_record/relation/predicate_builder/polymorphic_array_value.rb @@ -0,0 +1,53 @@ +# frozen_string_literal: true + +module ActiveRecord + class PredicateBuilder + class PolymorphicArrayValue # :nodoc: + def initialize(associated_table, values) + @associated_table = associated_table + @values = values + end + + def queries + type_to_ids_mapping.map do |type, ids| + { + associated_table.association_foreign_type.to_s => type, + associated_table.association_foreign_key.to_s => ids + } + end + end + + private + attr_reader :associated_table, :values + + def type_to_ids_mapping + default_hash = Hash.new { |hsh, key| hsh[key] = [] } + values.each_with_object(default_hash) do |value, hash| + hash[klass(value).polymorphic_name] << convert_to_id(value) + end + end + + def primary_key(value) + associated_table.association_join_primary_key(klass(value)) + end + + def klass(value) + case value + when Base + value.class + when Relation + value.klass + end + end + + def convert_to_id(value) + case value + when Base + value._read_attribute(primary_key(value)) + when Relation + value.select(primary_key(value)) + end + 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 5db778e19c..44bb2c7ab6 100644 --- a/activerecord/lib/active_record/relation/predicate_builder/range_handler.rb +++ b/activerecord/lib/active_record/relation/predicate_builder/range_handler.rb @@ -1,25 +1,41 @@ +# frozen_string_literal: true + module ActiveRecord class PredicateBuilder class RangeHandler # :nodoc: - RangeWithBinds = Struct.new(:begin, :end, :exclude_end?) + class RangeWithBinds < Struct.new(:begin, :end) + def exclude_end? + false + end + 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? + begin_bind = predicate_builder.build_bind_attribute(attribute.name, value.begin) + end_bind = predicate_builder.build_bind_attribute(attribute.name, value.end) + + if begin_bind.value.infinity? + if end_bind.value.infinity? attribute.not_in([]) elsif value.exclude_end? - attribute.lt(value.end) + attribute.lt(end_bind) else - attribute.lteq(value.end) + attribute.lteq(end_bind) end - elsif value.end.respond_to?(:infinite?) && value.end.infinite? - attribute.gteq(value.begin) + elsif end_bind.value.infinity? + attribute.gteq(begin_bind) elsif value.exclude_end? - attribute.gteq(value.begin).and(attribute.lt(value.end)) + attribute.gteq(begin_bind).and(attribute.lt(end_bind)) else - attribute.between(value) + attribute.between(RangeWithBinds.new(begin_bind, end_bind)) end end + + private + attr_reader :predicate_builder end end end diff --git a/activerecord/lib/active_record/relation/predicate_builder/relation_handler.rb b/activerecord/lib/active_record/relation/predicate_builder/relation_handler.rb index 8a910a82fe..c8bbfa5051 100644 --- a/activerecord/lib/active_record/relation/predicate_builder/relation_handler.rb +++ b/activerecord/lib/active_record/relation/predicate_builder/relation_handler.rb @@ -1,7 +1,13 @@ +# frozen_string_literal: true + module ActiveRecord class PredicateBuilder class RelationHandler # :nodoc: def call(attribute, value) + if value.eager_loading? + value = value.send(:apply_join_dependency) + end + if value.select_values.empty? value = value.select(value.arel_attribute(value.klass.primary_key)) end diff --git a/activerecord/lib/active_record/relation/query_attribute.rb b/activerecord/lib/active_record/relation/query_attribute.rb index a68e508fcc..f64bd30d38 100644 --- a/activerecord/lib/active_record/relation/query_attribute.rb +++ b/activerecord/lib/active_record/relation/query_attribute.rb @@ -1,8 +1,10 @@ -require "active_record/attribute" +# frozen_string_literal: true + +require "active_model/attribute" module ActiveRecord class Relation - class QueryAttribute < Attribute # :nodoc: + class QueryAttribute < ActiveModel::Attribute # :nodoc: def type_cast(value) value end @@ -14,6 +16,28 @@ module ActiveRecord def with_cast_value(value) QueryAttribute.new(name, value, type) end + + def nil? + !value_before_type_cast.is_a?(StatementCache::Substitute) && + (value_before_type_cast.nil? || value_for_database.nil?) + end + + def boundable? + return @_boundable if defined?(@_boundable) + nil? + @_boundable = true + rescue ::RangeError + @_boundable = false + end + + def infinity? + _infinity?(value_before_type_cast) || boundable? && _infinity?(value_for_database) + end + + private + def _infinity?(value) + value.respond_to?(:infinite?) && value.infinite? + end end end end diff --git a/activerecord/lib/active_record/relation/query_methods.rb b/activerecord/lib/active_record/relation/query_methods.rb index 4ee413c805..56497e11cb 100644 --- a/activerecord/lib/active_record/relation/query_methods.rb +++ b/activerecord/lib/active_record/relation/query_methods.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + require "active_record/relation/from_clause" require "active_record/relation/query_attribute" require "active_record/relation/where_clause" @@ -74,31 +76,6 @@ module ActiveRecord CODE end - def bound_attributes - if limit_value - limit_bind = Attribute.with_cast_value( - "LIMIT".freeze, - connection.sanitize_limit(limit_value), - Type.default_value, - ) - end - if offset_value - offset_bind = Attribute.with_cast_value( - "OFFSET".freeze, - offset_value.to_i, - Type.default_value, - ) - end - connection.combine_bind_parameters( - from_clause: from_clause.binds, - join_clause: arel.bind_values, - where_clause: where_clause.binds, - having_clause: having_clause.binds, - limit: limit_bind, - offset: offset_bind, - ) - end - alias extensions extending_values # Specify relationships to be included in the result set. For @@ -202,12 +179,13 @@ module ActiveRecord # Works in two unique ways. # - # First: takes a block so it can be used just like +Array#select+. + # First: takes a block so it can be used just like <tt>Array#select</tt>. # # Model.all.select { |m| m.field == value } # # This will build an array of objects from the database for the scope, - # converting them into an array and iterating through them using +Array#select+. + # converting them into an array and iterating through them using + # <tt>Array#select</tt>. # # Second: Modifies the SELECT statement for the query so that only certain # fields are retrieved: @@ -248,11 +226,12 @@ module ActiveRecord return super() end - raise ArgumentError, "Call this with at least one field" if fields.empty? + raise ArgumentError, "Call `select' with at least one field" if fields.empty? spawn._select!(*fields) end def _select!(*fields) # :nodoc: + fields.reject!(&:blank?) fields.flatten! fields.map! do |field| klass.attribute_alias?(field) ? klass.attribute_alias(field).to_sym : field @@ -317,6 +296,7 @@ module ActiveRecord spawn.order!(*args) end + # Same as #order but operates on relation in-place instead of copying. def order!(*args) # :nodoc: preprocess_order_args(args) @@ -338,6 +318,7 @@ module ActiveRecord spawn.reorder!(*args) end + # Same as #reorder but operates on relation in-place instead of copying. def reorder!(*args) # :nodoc: preprocess_order_args(args) @@ -347,8 +328,8 @@ module ActiveRecord end VALID_UNSCOPING_VALUES = Set.new([:where, :select, :group, :order, :lock, - :limit, :offset, :joins, :includes, :from, - :readonly, :having]) + :limit, :offset, :joins, :left_outer_joins, + :includes, :from, :readonly, :having]) # Removes an unwanted relation that is already defined on a chain of relations. # This is useful when passing around chains of relations and would like to @@ -395,10 +376,11 @@ module ActiveRecord args.each do |scope| case scope when Symbol + scope = :left_outer_joins if scope == :left_joins 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) + set_value(scope, DEFAULT_VALUES[scope]) when Hash scope.each do |key, target_value| if key != :where @@ -463,16 +445,14 @@ module ActiveRecord # => SELECT "users".* FROM "users" LEFT OUTER JOIN "posts" ON "posts"."user_id" = "users"."id" # def left_outer_joins(*args) - check_if_method_has_arguments!(:left_outer_joins, args) - - args.compact! - args.flatten! - + check_if_method_has_arguments!(__callee__, args) spawn.left_outer_joins!(*args) end alias :left_joins :left_outer_joins def left_outer_joins!(*args) # :nodoc: + args.compact! + args.flatten! self.left_outer_joins_values += args self end @@ -657,6 +637,7 @@ module ActiveRecord self.where_clause = self.where_clause.or(other.where_clause) self.having_clause = having_clause.or(other.having_clause) + self.references_values += other.references_values self end @@ -797,7 +778,7 @@ module ActiveRecord value = sanitize_forbidden_attributes(value) self.create_with_value = create_with_value.merge(value) else - self.create_with_value = {} + self.create_with_value = FROZEN_EMPTY_HASH end self @@ -913,21 +894,33 @@ module ActiveRecord self end + def skip_query_cache!(value = true) # :nodoc: + self.skip_query_cache_value = value + self + end + + def skip_preloading! # :nodoc: + self.skip_preloading_value = true + self + end + # Returns the Arel object associated with the relation. - def arel # :nodoc: - @arel ||= build_arel + def arel(aliases = nil) # :nodoc: + @arel ||= build_arel(aliases) end # Returns a relation value with a given name def get_value(name) # :nodoc: - @values[name] || default_value_for(name) + @values.fetch(name, DEFAULT_VALUES[name]) end - # Sets the relation value with the given name - def set_value(name, value) # :nodoc: - assert_mutability! - @values[name] = value - end + protected + + # Sets the relation value with the given name + def set_value(name, value) # :nodoc: + assert_mutability! + @values[name] = value + end private @@ -936,16 +929,30 @@ module ActiveRecord raise ImmutableRelation if defined?(@arel) && @arel end - def build_arel + def build_arel(aliases) arel = Arel::SelectManager.new(table) - 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? + aliases = build_joins(arel, joins_values.flatten, aliases) unless joins_values.empty? + build_left_outer_joins(arel, left_outer_joins_values.flatten, aliases) unless left_outer_joins_values.empty? arel.where(where_clause.ast) unless where_clause.empty? arel.having(having_clause.ast) unless having_clause.empty? - arel.take(Arel::Nodes::BindParam.new) if limit_value - arel.skip(Arel::Nodes::BindParam.new) if offset_value + if limit_value + limit_attribute = ActiveModel::Attribute.with_cast_value( + "LIMIT".freeze, + connection.sanitize_limit(limit_value), + Type.default_value, + ) + arel.take(Arel::Nodes::BindParam.new(limit_attribute)) + end + if offset_value + offset_attribute = ActiveModel::Attribute.with_cast_value( + "OFFSET".freeze, + offset_value.to_i, + Type.default_value, + ) + arel.skip(Arel::Nodes::BindParam.new(offset_attribute)) + end arel.group(*arel_columns(group_values.uniq.reject(&:blank?))) unless group_values.empty? build_order(arel) @@ -964,6 +971,9 @@ module ActiveRecord name = from_clause.name case opts when Relation + if opts.eager_loading? + opts = opts.send(:apply_join_dependency) + end name ||= "subquery" opts.arel.as(name.to_s) else @@ -971,20 +981,22 @@ module ActiveRecord end end - def build_left_outer_joins(manager, outer_joins) + def build_left_outer_joins(manager, outer_joins, aliases) buckets = outer_joins.group_by do |join| case join when Hash, Symbol, Array :association_join + when ActiveRecord::Associations::JoinDependency + :stashed_join else raise ArgumentError, "only Hash, Symbol and Array are allowed" end end - build_join_query(manager, buckets, Arel::Nodes::OuterJoin) + build_join_query(manager, buckets, Arel::Nodes::OuterJoin, aliases) end - def build_joins(manager, joins) + def build_joins(manager, joins, aliases) buckets = joins.group_by do |join| case join when String @@ -1000,38 +1012,31 @@ module ActiveRecord end end - build_join_query(manager, buckets, Arel::Nodes::InnerJoin) + build_join_query(manager, buckets, Arel::Nodes::InnerJoin, aliases) end - def build_join_query(manager, buckets, join_type) + def build_join_query(manager, buckets, join_type, aliases) buckets.default = [] - 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 + association_joins = buckets[:association_join] + stashed_joins = buckets[:stashed_join] + join_nodes = buckets[:join_node].uniq + string_joins = buckets[:string_join].map(&:strip).uniq - join_list = join_nodes + convert_join_strings_to_ast(manager, string_joins) + join_list = join_nodes + convert_join_strings_to_ast(string_joins) + alias_tracker = alias_tracker(join_list, aliases) - join_dependency = ActiveRecord::Associations::JoinDependency.new( - @klass, - association_joins, - join_list - ) + join_dependency = construct_join_dependency(association_joins) - join_infos = join_dependency.join_constraints stashed_association_joins, join_type - - join_infos.each do |info| - info.joins.each { |join| manager.from(join) } - manager.bind_values.concat info.binds - end + joins = join_dependency.join_constraints(stashed_joins, join_type, alias_tracker) + joins.each { |join| manager.from(join) } manager.join_sources.concat(join_list) - manager + alias_tracker.aliases end - def convert_join_strings_to_ast(table, joins) + def convert_join_strings_to_ast(joins) joins .flatten .reject(&:blank?) @@ -1041,17 +1046,21 @@ module ActiveRecord def build_select(arel) if select_values.any? arel.project(*arel_columns(select_values.uniq)) + elsif klass.ignored_columns.any? + arel.project(*klass.column_names.map { |field| arel_attribute(field) }) else - arel.project(@klass.arel_table[Arel.star]) + arel.project(table[Arel.star]) end end def arel_columns(columns) - columns.map do |field| + columns.flat_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) + elsif Proc === field + field.call else field end @@ -1077,7 +1086,7 @@ module ActiveRecord end o.split(",").map! do |s| s.strip! - s.gsub!(/\sasc\Z/i, " DESC") || s.gsub!(/\sdesc\Z/i, " ASC") || s.concat(" DESC") + s.gsub!(/\sasc\Z/i, " DESC") || s.gsub!(/\sdesc\Z/i, " ASC") || (s << " DESC") end else o @@ -1086,6 +1095,10 @@ module ActiveRecord end def does_not_support_reverse?(order) + # Account for String subclasses like Arel::Nodes::SqlLiteral that + # override methods like #count. + order = String.new(order) unless order.instance_of?(String) + # Uses SQL function with multiple arguments. (order.include?(",") && order.split(",").find { |section| section.count("(") != section.count(")") }) || # Uses "nulls first" like construction. @@ -1100,27 +1113,35 @@ module ActiveRecord end VALID_DIRECTIONS = [:asc, :desc, :ASC, :DESC, - "asc", "desc", "ASC", "DESC"] # :nodoc: + "asc", "desc", "ASC", "DESC"].to_set # :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) + unless VALID_DIRECTIONS.include?(value) + raise ArgumentError, + "Direction \"#{value}\" is invalid. Valid directions are: #{VALID_DIRECTIONS.to_a.inspect}" + end end end end def preprocess_order_args(order_args) order_args.map! do |arg| - klass.send(:sanitize_sql_for_order, arg) + klass.sanitize_sql_for_order(arg) end order_args.flatten! + + @klass.disallow_raw_sql!( + order_args.flat_map { |a| a.is_a?(Hash) ? a.keys : a }, + permit: AttributeMethods::ClassMethods::COLUMN_NAME_WITH_ORDER + ) + validate_order_args(order_args) references = order_args.grep(String) - references.map! { |arg| arg =~ /^([a-zA-Z]\w*)\.(\w+)/ && $1 }.compact! + references.map! { |arg| arg =~ /^\W?(\w+)\W?\./ && $1 }.compact! references!(references) if references.any? # if a symbol is given we prepend the quoted table name @@ -1130,7 +1151,12 @@ module ActiveRecord arel_attribute(arg).asc when Hash arg.map { |field, dir| - arel_attribute(field).send(dir.downcase) + case field + when Arel::Nodes::SqlLiteral + field.send(dir.downcase) + else + arel_attribute(field).send(dir.downcase) + end } else arg @@ -1160,7 +1186,7 @@ module ActiveRecord end end - STRUCTURAL_OR_METHODS = Relation::VALUE_METHODS - [:extending, :where, :having] + STRUCTURAL_OR_METHODS = Relation::VALUE_METHODS - [:extending, :where, :having, :unscope, :references] def structurally_incompatible_values_for_or(other) STRUCTURAL_OR_METHODS.reject do |method| get_value(method) == other.get_value(method) @@ -1172,23 +1198,15 @@ module ActiveRecord end alias having_clause_factory where_clause_factory - 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 + DEFAULT_VALUES = { + create_with: FROZEN_EMPTY_HASH, + where: Relation::WhereClause.empty, + having: Relation::WhereClause.empty, + from: Relation::FromClause.empty + } + + Relation::MULTI_VALUE_METHODS.each do |value| + DEFAULT_VALUES[value] ||= FROZEN_EMPTY_ARRAY 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 31544c730e..a7d07d23e1 100644 --- a/activerecord/lib/active_record/relation/record_fetch_warning.rb +++ b/activerecord/lib/active_record/relation/record_fetch_warning.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + module ActiveRecord class Relation module RecordFetchWarning diff --git a/activerecord/lib/active_record/relation/spawn_methods.rb b/activerecord/lib/active_record/relation/spawn_methods.rb index ada89b5ec3..562e04194c 100644 --- a/activerecord/lib/active_record/relation/spawn_methods.rb +++ b/activerecord/lib/active_record/relation/spawn_methods.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + require "active_support/core_ext/hash/except" require "active_support/core_ext/hash/slice" require "active_record/relation/merger" @@ -67,7 +69,7 @@ module ActiveRecord private def relation_with(values) - result = Relation.create(klass, table, predicate_builder, values) + result = Relation.create(klass, values: values) result.extend(*extending_values) if extending_values.any? result end diff --git a/activerecord/lib/active_record/relation/where_clause.rb b/activerecord/lib/active_record/relation/where_clause.rb index 417b24c7bb..a502713e56 100644 --- a/activerecord/lib/active_record/relation/where_clause.rb +++ b/activerecord/lib/active_record/relation/where_clause.rb @@ -1,65 +1,63 @@ +# frozen_string_literal: true + module ActiveRecord class Relation class WhereClause # :nodoc: - attr_reader :binds - delegate :any?, :empty?, to: :predicates - def initialize(predicates, binds) + def initialize(predicates) @predicates = predicates - @binds = binds end def +(other) WhereClause.new( predicates + other.predicates, - binds + other.binds, + ) + end + + def -(other) + WhereClause.new( + predicates - other.predicates, ) end def merge(other) WhereClause.new( predicates_unreferenced_by(other) + other.predicates, - non_conflicting_binds(other) + other.binds, ) end def except(*columns) - WhereClause.new(*except_predicates_and_binds(columns)) + WhereClause.new(except_predicates(columns)) end def or(other) - if empty? - self - elsif other.empty? - other + left = self - other + common = self - left + right = other - common + + if left.empty? || right.empty? + common else - WhereClause.new( - [ast.or(other.ast)], - binds + other.binds + or_clause = WhereClause.new( + [left.ast.or(right.ast)], ) + common + or_clause end end def to_h(table_name = nil) - equalities = predicates.grep(Arel::Nodes::Equality) + equalities = equalities(predicates) if table_name equalities = equalities.select do |node| node.left.relation.name == table_name end end - binds = self.binds.map { |attr| [attr.name, attr.value] }.to_h - equalities.map { |node| - name = node.left.name - [name, binds.fetch(name.to_s) { - case node.right - when Array then node.right.map(&:val) - when Arel::Nodes::Casted, Arel::Nodes::Quoted - node.right.val - end - }] + name = node.left.name.to_s + value = extract_node_value(node.right) + [name, value] }.to_h end @@ -69,20 +67,17 @@ module ActiveRecord def ==(other) other.is_a?(WhereClause) && - predicates == other.predicates && - binds == other.binds + predicates == other.predicates end def invert - WhereClause.new(inverted_predicates, binds) + WhereClause.new(inverted_predicates) end def self.empty - @empty ||= new([], []) + @empty ||= new([]) end - # TODO Change this to private once we've dropped Ruby 2.2 support. - # Workaround for Ruby 2.2 "private attribute?" warning. protected attr_reader :predicates @@ -95,6 +90,20 @@ module ActiveRecord end private + def equalities(predicates) + equalities = [] + + predicates.each do |node| + case node + when Arel::Nodes::Equality + equalities << node + when Arel::Nodes::And + equalities.concat equalities(node.children) + end + end + + equalities + end def predicates_unreferenced_by(other) predicates.reject do |n| @@ -106,12 +115,6 @@ module ActiveRecord 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 inverted_predicates predicates.map { |node| invert_predicate(node) } end @@ -131,43 +134,22 @@ module ActiveRecord end end - def except_predicates_and_binds(columns) - except_binds = [] - binds_index = 0 - - predicates = self.predicates.reject do |node| - except = \ - 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 - binds_contains = node.grep(Arel::Nodes::BindParam).size - subrelation = (node.left.kind_of?(Arel::Attributes::Attribute) ? node.left : node.right) - columns.include?(subrelation.name.to_s) - end - - if except && binds_contains > 0 - (binds_index...(binds_index + binds_contains)).each do |i| - except_binds[i] = true - end - - binds_index += binds_contains + def except_predicates(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 - - except end - - binds = self.binds.reject.with_index do |_, i| - except_binds[i] - end - - [predicates, binds] end def predicates_with_wrapped_sql_literals non_empty_predicates.map do |node| - if Arel::Nodes::Equality === node - node - else + case node + when Arel::Nodes::SqlLiteral, ::String wrap_sql_literal(node) + else node end end end @@ -183,6 +165,22 @@ module ActiveRecord end Arel::Nodes::Grouping.new(node) end + + def extract_node_value(node) + case node + when Array + node.map { |v| extract_node_value(v) } + when Arel::Nodes::Casted, Arel::Nodes::Quoted + node.val + when Arel::Nodes::BindParam + value = node.value + if value.respond_to?(:value_before_type_cast) + value.value_before_type_cast + else + value + end + end + 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 04bee73e8f..c1b3eea9df 100644 --- a/activerecord/lib/active_record/relation/where_clause_factory.rb +++ b/activerecord/lib/active_record/relation/where_clause_factory.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + module ActiveRecord class Relation class WhereClauseFactory # :nodoc: @@ -9,69 +11,23 @@ module ActiveRecord def build(opts, other) case opts when String, Array - parts = [klass.send(:sanitize_sql, other.empty? ? opts : ([opts] + other))] + parts = [klass.sanitize_sql(other.empty? ? opts : ([opts] + other))] when Hash attributes = predicate_builder.resolve_column_aliases(opts) - attributes = klass.send(:expand_hash_conditions_for_aggregates, attributes) attributes.stringify_keys! - if perform_case_sensitive?(options = other.last) - parts, binds = build_for_case_sensitive(attributes, options) - else - attributes, binds = predicate_builder.create_binds(attributes) - parts = predicate_builder.build_from_hash(attributes) - end + parts = predicate_builder.build_from_hash(attributes) when Arel::Nodes::Node parts = [opts] else raise ArgumentError, "Unsupported argument type: #{opts} (#{opts.class})" end - WhereClause.new(parts, binds || []) + WhereClause.new(parts) end - # TODO Change this to private once we've dropped Ruby 2.2 support. - # Workaround for Ruby 2.2 "private attribute?" warning. - protected - - attr_reader :klass, :predicate_builder - private - - def perform_case_sensitive?(options) - options && options.key?(:case_sensitive) - end - - def build_for_case_sensitive(attributes, options) - parts, binds = [], [] - table = klass.arel_table - - attributes.each do |attribute, value| - if reflection = klass._reflect_on_association(attribute) - attribute = reflection.foreign_key.to_s - value = value[reflection.klass.primary_key] unless value.nil? - end - - if value.nil? - parts << table[attribute].eq(value) - else - column = klass.column_for_attribute(attribute) - - binds << predicate_builder.send(:build_bind_param, attribute, value) - value = Arel::Nodes::BindParam.new - - predicate = if options[:case_sensitive] - klass.connection.case_sensitive_comparison(table, attribute, column, value) - else - klass.connection.case_insensitive_comparison(table, attribute, column, value) - end - - parts << predicate - end - end - - [parts, binds] - end + attr_reader :klass, :predicate_builder end end end |