diff options
Diffstat (limited to 'activerecord/lib')
11 files changed, 79 insertions, 38 deletions
diff --git a/activerecord/lib/active_record/associations/association_scope.rb b/activerecord/lib/active_record/associations/association_scope.rb index 8a17f0ced4..0209ce36df 100644 --- a/activerecord/lib/active_record/associations/association_scope.rb +++ b/activerecord/lib/active_record/associations/association_scope.rb @@ -20,7 +20,7 @@ module ActiveRecord # It's okay to just apply all these like this. The options will only be present if the # association supports that option; this is enforced by the association builder. scope = scope.apply_finder_options(options.slice( - :readonly, :include, :eager_load, :order, :limit, :joins, :group, :having, :offset, :select)) + :readonly, :include, :references, :order, :limit, :joins, :group, :having, :offset, :select)) if options[:through] && !options[:include] scope = scope.includes(source_options[:include]) @@ -90,8 +90,11 @@ module ActiveRecord scope = scope.joins(join(foreign_table, constraint)) - unless conditions.empty? - scope = scope.where(sanitize(conditions, table)) + conditions.each do |condition| + condition = interpolate(condition) + condition = { (table.table_alias || table.name) => condition } unless i == 0 + + scope = scope.where(condition) end end end diff --git a/activerecord/lib/active_record/associations/builder/association.rb b/activerecord/lib/active_record/associations/builder/association.rb index 776f0d0469..6e2e5f9de0 100644 --- a/activerecord/lib/active_record/associations/builder/association.rb +++ b/activerecord/lib/active_record/associations/builder/association.rb @@ -1,7 +1,7 @@ module ActiveRecord::Associations::Builder class Association #:nodoc: class_attribute :valid_options - self.valid_options = [:class_name, :foreign_key, :select, :conditions, :include, :eager_load, :extend, :readonly, :validate] + self.valid_options = [:class_name, :foreign_key, :select, :conditions, :include, :extend, :readonly, :validate, :references] # Set by subclasses class_attribute :macro diff --git a/activerecord/lib/active_record/associations/join_dependency/join_association.rb b/activerecord/lib/active_record/associations/join_dependency/join_association.rb index 03963ab060..0d7d28e458 100644 --- a/activerecord/lib/active_record/associations/join_dependency/join_association.rb +++ b/activerecord/lib/active_record/associations/join_dependency/join_association.rb @@ -95,8 +95,11 @@ module ActiveRecord conditions = self.conditions[i].dup conditions << { reflection.type => foreign_klass.base_class.name } if reflection.type - unless conditions.empty? - constraint = constraint.and(sanitize(conditions, table)) + conditions.each do |condition| + condition = active_record.send(:sanitize_sql, interpolate(condition), table.table_alias || table.name) + condition = Arel.sql(condition) unless condition.is_a?(Arel::Node) + + constraint = constraint.and(condition) end relation.from(join(table, constraint)) diff --git a/activerecord/lib/active_record/associations/join_helper.rb b/activerecord/lib/active_record/associations/join_helper.rb index f83138195c..cea6ad6944 100644 --- a/activerecord/lib/active_record/associations/join_helper.rb +++ b/activerecord/lib/active_record/associations/join_helper.rb @@ -40,16 +40,6 @@ module ActiveRecord def join(table, constraint) table.create_join(table, table.create_on(constraint), join_type) end - - def sanitize(conditions, table) - conditions = conditions.map do |condition| - condition = active_record.send(:sanitize_sql, interpolate(condition), table.table_alias || table.name) - condition = Arel.sql(condition) unless condition.is_a?(Arel::Node) - condition - end - - conditions.length == 1 ? conditions.first : Arel::Nodes::And.new(conditions) - end end end end diff --git a/activerecord/lib/active_record/associations/preloader/association.rb b/activerecord/lib/active_record/associations/preloader/association.rb index 298decb0f1..253998fb23 100644 --- a/activerecord/lib/active_record/associations/preloader/association.rb +++ b/activerecord/lib/active_record/associations/preloader/association.rb @@ -95,12 +95,11 @@ module ActiveRecord def build_scope scope = klass.scoped - scope = scope.where(process_conditions(options[:conditions])) - scope = scope.where(process_conditions(preload_options[:conditions])) + scope = scope.where(interpolate(options[:conditions])) + scope = scope.where(interpolate(preload_options[:conditions])) scope = scope.select(preload_options[:select] || options[:select] || table[Arel.star]) scope = scope.includes(preload_options[:include] || options[:include]) - scope = scope.eager_load(preload_options[:eager_load] || options[:eager_load]) if options[:as] scope = scope.where( @@ -113,13 +112,11 @@ module ActiveRecord scope end - def process_conditions(conditions) + def interpolate(conditions) if conditions.respond_to?(:to_proc) - conditions = klass.send(:instance_eval, &conditions) - end - - if conditions - klass.send(:sanitize_sql, conditions) + klass.send(:instance_eval, &conditions) + else + conditions end end end diff --git a/activerecord/lib/active_record/associations/preloader/through_association.rb b/activerecord/lib/active_record/associations/preloader/through_association.rb index 97898c53ae..ad6374d09a 100644 --- a/activerecord/lib/active_record/associations/preloader/through_association.rb +++ b/activerecord/lib/active_record/associations/preloader/through_association.rb @@ -53,7 +53,6 @@ module ActiveRecord else if options[:conditions] through_options[:include] = options[:include] || options[:source] - through_options[:eager_load] = options[:eager_load] || options[:source] through_options[:conditions] = options[:conditions] end diff --git a/activerecord/lib/active_record/querying.rb b/activerecord/lib/active_record/querying.rb index 09da9ad1d1..94e34e1bd4 100644 --- a/activerecord/lib/active_record/querying.rb +++ b/activerecord/lib/active_record/querying.rb @@ -8,7 +8,7 @@ module ActiveRecord delegate :find_each, :find_in_batches, :to => :scoped delegate :select, :group, :order, :except, :reorder, :limit, :offset, :joins, :where, :preload, :eager_load, :includes, :from, :lock, :readonly, - :having, :create_with, :uniq, :to => :scoped + :having, :create_with, :uniq, :references, :to => :scoped delegate :count, :average, :minimum, :maximum, :sum, :calculate, :pluck, :to => :scoped # Executes a custom SQL query against your database and returns all the results. The results will diff --git a/activerecord/lib/active_record/relation.rb b/activerecord/lib/active_record/relation.rb index bf1de4ba9d..01019db2cc 100644 --- a/activerecord/lib/active_record/relation.rb +++ b/activerecord/lib/active_record/relation.rb @@ -7,7 +7,7 @@ module ActiveRecord class Relation JoinOperation = Struct.new(:relation, :join_class, :on) ASSOCIATION_METHODS = [:includes, :eager_load, :preload] - MULTI_VALUE_METHODS = [:select, :group, :order, :joins, :where, :having, :bind] + MULTI_VALUE_METHODS = [:select, :group, :order, :joins, :where, :having, :bind, :references] SINGLE_VALUE_METHODS = [:limit, :offset, :lock, :readonly, :from, :reordering, :reverse_order, :uniq] include FinderMethods, Calculations, SpawnMethods, QueryMethods, Batches, Explain, Delegation @@ -521,14 +521,25 @@ module ActiveRecord # always convert table names to downcase as in Oracle quoted table names are in uppercase joined_tables = joined_tables.flatten.compact.map { |t| t.downcase }.uniq + string_tables = tables_in_string(to_sql) - referenced_tables = (tables_in_string(to_sql) - joined_tables) - if referenced_tables.any? + if (references_values - joined_tables).any? + true + elsif (string_tables - joined_tables).any? ActiveSupport::Deprecation.warn( - "Your query appears to reference tables (#{referenced_tables.join(', ')}) that are not " \ - "explicitly joined. This implicit joining is deprecated, so you must explicitly " \ - "reference the tables. For example, instead of Author.includes(:posts).where(\"posts.name = 'foo'\"), " \ - "you should write Author.eager_load(:posts).where(\"posts.name = 'foo'\")." + "It looks like you are eager loading table(s) (one of: #{string_tables.join(', ')}) " \ + "that are referenced in a string SQL snippet. For example: \n" \ + "\n" \ + " Post.includes(:comments).where(\"comments.title = 'foo'\")\n" \ + "\n" \ + "Currently, Active Record recognises the table in the string, and knows to JOIN the " \ + "comments table to the query, rather than loading comments in a separate query. " \ + "However, doing this without writing a full-blown SQL parser is inherently flawed. " \ + "Since we don't want to write an SQL parser, we are removing this functionality. " \ + "From now on, you must explicitly tell Active Record when you are referencing a table " \ + "from a string:\n" \ + "\n" \ + " Post.includes(:comments).where(\"comments.title = 'foo'\").references(:comments)\n\n" ) true else diff --git a/activerecord/lib/active_record/relation/predicate_builder.rb b/activerecord/lib/active_record/relation/predicate_builder.rb index 88081edae2..1d04e763f6 100644 --- a/activerecord/lib/active_record/relation/predicate_builder.rb +++ b/activerecord/lib/active_record/relation/predicate_builder.rb @@ -21,6 +21,18 @@ module ActiveRecord predicates.flatten end + def self.references(attributes) + references = attributes.map do |key, value| + if value.is_a?(Hash) + key + else + key = key.to_s + key.split('.').first.to_sym if key.include?('.') + end + end + references.compact + end + private def self.build(attribute, value) case value diff --git a/activerecord/lib/active_record/relation/query_methods.rb b/activerecord/lib/active_record/relation/query_methods.rb index 44ff8f7b22..b5f202ef6a 100644 --- a/activerecord/lib/active_record/relation/query_methods.rb +++ b/activerecord/lib/active_record/relation/query_methods.rb @@ -10,7 +10,7 @@ module ActiveRecord :where_values, :having_values, :bind_values, :limit_value, :offset_value, :lock_value, :readonly_value, :create_with_value, :from_value, :reordering_value, :reverse_order_value, - :uniq_value + :uniq_value, :references_values def includes(*args) args.reject! {|a| a.blank? } @@ -38,6 +38,24 @@ module ActiveRecord relation end + # Used to indicate that an association is referenced by an SQL string, and should + # therefore be JOINed in any query rather than loaded separately. + # + # For example: + # + # User.includes(:posts).where("posts.name = 'foo'") + # # => Doesn't JOIN the posts table, resulting in an error. + # + # User.includes(:posts).where("posts.name = 'foo'").references(:posts) + # # => Query now knows the string references posts, so adds a JOIN + def references(*args) + return self if args.blank? + + relation = clone + relation.references_values = (references_values + args.flatten.map(&:to_s)).uniq + relation + end + # Works in two unique ways. # # First: takes a block so it can be used just like Array#select. @@ -88,8 +106,14 @@ module ActiveRecord def order(*args) return self if args.blank? + args = args.flatten + references = args.reject { |arg| Arel::Node === arg } + .map { |arg| arg =~ /^([a-zA-Z]\w*)\.(\w+)/ && $1 } + .compact + relation = clone - relation.order_values += args.flatten + relation = relation.references(references) if references.any? + relation.order_values += args relation end @@ -133,6 +157,7 @@ module ActiveRecord return self if opts.blank? relation = clone + relation = relation.references(PredicateBuilder.references(opts)) if Hash === opts relation.where_values += build_where(opts, rest) relation end @@ -141,6 +166,7 @@ module ActiveRecord return self if opts.blank? relation = clone + relation = relation.references(PredicateBuilder.references(opts)) if Hash === opts relation.having_values += build_where(opts, rest) relation end diff --git a/activerecord/lib/active_record/relation/spawn_methods.rb b/activerecord/lib/active_record/relation/spawn_methods.rb index de639a48f2..7131aa29b6 100644 --- a/activerecord/lib/active_record/relation/spawn_methods.rb +++ b/activerecord/lib/active_record/relation/spawn_methods.rb @@ -122,7 +122,7 @@ module ActiveRecord result end - VALID_FIND_OPTIONS = [ :conditions, :include, :joins, :limit, :offset, :extend, :eager_load, + VALID_FIND_OPTIONS = [ :conditions, :include, :joins, :limit, :offset, :extend, :references, :order, :select, :readonly, :group, :having, :from, :lock ] def apply_finder_options(options) @@ -133,7 +133,7 @@ module ActiveRecord finders = options.dup finders.delete_if { |key, value| value.nil? && key != :limit } - ([:joins, :select, :group, :order, :having, :limit, :offset, :from, :lock, :readonly, :eager_load] & finders.keys).each do |finder| + ((VALID_FIND_OPTIONS - [:conditions, :include, :extend]) & finders.keys).each do |finder| relation = relation.send(finder, finders[finder]) end |