diff options
Diffstat (limited to 'activerecord/lib')
19 files changed, 233 insertions, 201 deletions
diff --git a/activerecord/lib/active_record.rb b/activerecord/lib/active_record.rb index a524dc50a1..cf439b0dc0 100644 --- a/activerecord/lib/active_record.rb +++ b/activerecord/lib/active_record.rb @@ -54,6 +54,7 @@ module ActiveRecord autoload :QueryMethods autoload :FinderMethods autoload :CalculationMethods + autoload :PredicateBuilder end autoload :Base diff --git a/activerecord/lib/active_record/associations.rb b/activerecord/lib/active_record/associations.rb index f0bad6c3ba..d74b21b690 100755 --- a/activerecord/lib/active_record/associations.rb +++ b/activerecord/lib/active_record/associations.rb @@ -1465,8 +1465,7 @@ module ActiveRecord after_destroy(method_name) end - def find_with_associations(options = {}, join_dependency = nil) - join_dependency ||= JoinDependency.new(self, merge_includes(scope(:find, :include), options[:include]), options[:joins]) + def find_with_associations(options = {}, join_dependency) rows = select_all_rows(options, join_dependency) join_dependency.instantiate(rows) rescue ThrowResult @@ -1488,7 +1487,7 @@ module ActiveRecord dependent_conditions = [] dependent_conditions << "#{reflection.primary_key_name} = \#{record.#{reflection.name}.send(:owner_quoted_id)}" dependent_conditions << "#{reflection.options[:as]}_type = '#{base_class.name}'" if reflection.options[:as] - dependent_conditions << sanitize_sql(reflection.options[:conditions], reflection.quoted_table_name) if reflection.options[:conditions] + dependent_conditions << sanitize_sql(reflection.options[:conditions], reflection.table_name) if reflection.options[:conditions] dependent_conditions << extra_conditions if extra_conditions dependent_conditions = dependent_conditions.collect {|where| "(#{where})" }.join(" AND ") dependent_conditions = dependent_conditions.gsub('@', '\@') @@ -1706,7 +1705,7 @@ module ActiveRecord def construct_finder_arel_with_included_associations(options, join_dependency) scope = scope(:find) - relation = arel_table + relation = active_relation for association in join_dependency.join_associations relation = association.join_relation(relation) @@ -1751,7 +1750,7 @@ module ActiveRecord def construct_finder_sql_for_association_limiting(options, join_dependency) scope = scope(:find) - relation = arel_table(options[:from]) + relation = active_relation for association in join_dependency.join_associations relation = association.join_relation(relation) @@ -1764,89 +1763,12 @@ module ActiveRecord order(construct_order(options[:order], scope)). limit(construct_limit(options[:limit], scope)). offset(construct_limit(options[:offset], scope)). + from(options[:from]). select(connection.distinct("#{connection.quote_table_name table_name}.#{primary_key}", construct_order(options[:order], scope(:find)).join(","))) relation.to_sql end - def tables_in_string(string) - return [] if string.blank? - string.scan(/([a-zA-Z_][\.\w]+).?\./).flatten - end - - def tables_in_hash(hash) - return [] if hash.blank? - tables = hash.map do |key, value| - if value.is_a?(Hash) - key.to_s - else - tables_in_string(key) if key.is_a?(String) - end - end - tables.flatten.compact - end - - def conditions_tables(options) - # look in both sets of conditions - conditions = [scope(:find, :conditions), options[:conditions]].inject([]) do |all, cond| - case cond - when nil then all - when Array then all << tables_in_string(cond.first) - when Hash then all << tables_in_hash(cond) - else all << tables_in_string(cond) - end - end - conditions.flatten - end - - def order_tables(options) - order = [options[:order], scope(:find, :order) ].join(", ") - return [] unless order && order.is_a?(String) - tables_in_string(order) - end - - def selects_tables(options) - select = options[:select] - return [] unless select && select.is_a?(String) - tables_in_string(select) - end - - def joined_tables(options) - scope = scope(:find) - joins = options[:joins] - merged_joins = scope && scope[:joins] && joins ? merge_joins(scope[:joins], joins) : (joins || scope && scope[:joins]) - [table_name] + case merged_joins - when Symbol, Hash, Array - if array_of_strings?(merged_joins) - tables_in_string(merged_joins.join(' ')) - else - join_dependency = ActiveRecord::Associations::ClassMethods::JoinDependency.new(self, merged_joins, nil) - join_dependency.join_associations.collect {|join_association| [join_association.aliased_join_table_name, join_association.aliased_table_name]}.flatten.compact - end - else - tables_in_string(merged_joins) - end - end - - # Checks if the conditions reference a table other than the current model table - def include_eager_conditions?(options, tables = nil, joined_tables = nil) - ((tables || conditions_tables(options)) - (joined_tables || joined_tables(options))).any? - end - - # Checks if the query order references a table other than the current model's table. - def include_eager_order?(options, tables = nil, joined_tables = nil) - ((tables || order_tables(options)) - (joined_tables || joined_tables(options))).any? - end - - def include_eager_select?(options, joined_tables = nil) - (selects_tables(options) - (joined_tables || joined_tables(options))).any? - end - - def references_eager_loaded_tables?(options) - joined_tables = joined_tables(options) - include_eager_order?(options, nil, joined_tables) || include_eager_conditions?(options, nil, joined_tables) || include_eager_select?(options, joined_tables) - end - def using_limitable_reflections?(reflections) reflections.reject { |r| [ :belongs_to, :has_one ].include?(r.macro) }.length.zero? end diff --git a/activerecord/lib/active_record/associations/association_collection.rb b/activerecord/lib/active_record/associations/association_collection.rb index 1ceb0dbf96..358db6df1d 100644 --- a/activerecord/lib/active_record/associations/association_collection.rb +++ b/activerecord/lib/active_record/associations/association_collection.rb @@ -21,7 +21,7 @@ module ActiveRecord construct_sql end - delegate :group, :order, :limit, :joins, :where, :preload, :eager_load, :from, :lock, :readonly, :having, :to => :scoped + delegate :group, :order, :limit, :joins, :where, :preload, :eager_load, :includes, :from, :lock, :readonly, :having, :to => :scoped def select(select = nil, &block) if block_given? @@ -58,7 +58,7 @@ module ActiveRecord find_scope = construct_scope[:find].slice(:conditions, :order) with_scope(:find => find_scope) do - relation = @reflection.klass.send(:construct_finder_arel_with_includes, options) + relation = @reflection.klass.send(:construct_finder_arel, options) case args.first when :first, :last, :all diff --git a/activerecord/lib/active_record/associations/association_proxy.rb b/activerecord/lib/active_record/associations/association_proxy.rb index 7d8f4670fa..022dd2ae9b 100644 --- a/activerecord/lib/active_record/associations/association_proxy.rb +++ b/activerecord/lib/active_record/associations/association_proxy.rb @@ -161,7 +161,7 @@ module ActiveRecord end # Forwards the call to the reflection class. - def sanitize_sql(sql, table_name = @reflection.klass.quoted_table_name) + def sanitize_sql(sql, table_name = @reflection.klass.table_name) @reflection.klass.send(:sanitize_sql, sql, table_name) end diff --git a/activerecord/lib/active_record/associations/has_and_belongs_to_many_association.rb b/activerecord/lib/active_record/associations/has_and_belongs_to_many_association.rb index 9569b0c6f9..bd05d1014c 100644 --- a/activerecord/lib/active_record/associations/has_and_belongs_to_many_association.rb +++ b/activerecord/lib/active_record/associations/has_and_belongs_to_many_association.rb @@ -44,7 +44,7 @@ module ActiveRecord if @reflection.options[:insert_sql] @owner.connection.insert(interpolate_sql(@reflection.options[:insert_sql], record)) else - relation = arel_table(@reflection.options[:join_table]) + relation = Arel::Table.new(@reflection.options[:join_table]) attributes = columns.inject({}) do |attrs, column| case column.name.to_s when @reflection.primary_key_name.to_s @@ -70,7 +70,7 @@ module ActiveRecord if sql = @reflection.options[:delete_sql] records.each { |record| @owner.connection.delete(interpolate_sql(sql, record)) } else - relation = arel_table(@reflection.options[:join_table]) + relation = Arel::Table.new(@reflection.options[:join_table]) relation.where(relation[@reflection.primary_key_name].eq(@owner.id). and(Arel::Predicates::In.new(relation[@reflection.association_foreign_key], records.map(&:id))) ).delete diff --git a/activerecord/lib/active_record/associations/has_many_association.rb b/activerecord/lib/active_record/associations/has_many_association.rb index be74ddfcf0..d3336cf2d2 100644 --- a/activerecord/lib/active_record/associations/has_many_association.rb +++ b/activerecord/lib/active_record/associations/has_many_association.rb @@ -69,7 +69,7 @@ module ActiveRecord when :delete_all @reflection.klass.delete(records.map { |record| record.id }) else - relation = arel_table(@reflection.table_name) + relation = Arel::Table.new(@reflection.table_name) relation.where(relation[@reflection.primary_key_name].eq(@owner.id). and(Arel::Predicates::In.new(relation[@reflection.klass.primary_key], records.map(&:id))) ).update(relation[@reflection.primary_key_name] => nil) diff --git a/activerecord/lib/active_record/autosave_association.rb b/activerecord/lib/active_record/autosave_association.rb index 44c668b619..98ab64537e 100644 --- a/activerecord/lib/active_record/autosave_association.rb +++ b/activerecord/lib/active_record/autosave_association.rb @@ -267,7 +267,7 @@ module ActiveRecord unless valid = association.valid? if reflection.options[:autosave] association.errors.each do |attribute, message| - attribute = "#{reflection.name}_#{attribute}" + attribute = "#{reflection.name}.#{attribute}" errors[attribute] << message if errors[attribute].empty? end else diff --git a/activerecord/lib/active_record/base.rb b/activerecord/lib/active_record/base.rb index c4bdbdad08..70776c7aa2 100755 --- a/activerecord/lib/active_record/base.rb +++ b/activerecord/lib/active_record/base.rb @@ -645,7 +645,7 @@ module ActiveRecord #:nodoc: options = args.extract_options! set_readonly_option!(options) - relation = construct_finder_arel_with_includes(options) + relation = construct_finder_arel(options) case args.first when :first, :last, :all @@ -655,7 +655,7 @@ module ActiveRecord #:nodoc: end end - delegate :select, :group, :order, :limit, :joins, :where, :preload, :eager_load, :from, :lock, :readonly, :having, :to => :scoped + delegate :select, :group, :order, :limit, :joins, :where, :preload, :eager_load, :includes, :from, :lock, :readonly, :having, :to => :scoped # A convenience wrapper for <tt>find(:first, *args)</tt>. You can pass in all the # same arguments to this method as you can to <tt>find(:first)</tt>. @@ -815,8 +815,8 @@ module ActiveRecord #:nodoc: # # # Delete multiple rows # Todo.delete([2,3,4]) - def delete(id) - delete_all([ "#{connection.quote_column_name(primary_key)} IN (?)", id ]) + def delete(id_or_array) + active_relation.where(construct_conditions(nil, scope(:find))).delete(id_or_array) end # Destroy an object (or multiple objects) that has the given id, the object is instantiated first, @@ -873,7 +873,7 @@ module ActiveRecord #:nodoc: def update_all(updates, conditions = nil, options = {}) scope = scope(:find) - relation = arel_table + relation = active_relation if conditions = construct_conditions(conditions, scope) relation = relation.where(Arel::SqlLiteral.new(conditions)) @@ -938,7 +938,7 @@ module ActiveRecord #:nodoc: # Both calls delete the affected posts all at once with a single DELETE statement. If you need to destroy dependent # associations or call your <tt>before_*</tt> or +after_destroy+ callbacks, use the +destroy_all+ method instead. def delete_all(conditions = nil) - arel_table.where(construct_conditions(conditions, scope(:find))).delete_all + active_relation.where(construct_conditions(conditions, scope(:find))).delete_all end # Returns the result of an SQL statement that should only include a COUNT(*) in the SELECT part. @@ -1391,7 +1391,8 @@ module ActiveRecord #:nodoc: # end def reset_column_information undefine_attribute_methods - @arel_table = @column_names = @columns = @columns_hash = @content_columns = @dynamic_methods_hash = @inheritance_column = nil + @column_names = @columns = @columns_hash = @content_columns = @dynamic_methods_hash = @inheritance_column = nil + @active_relation = @active_relation_engine = nil end def reset_column_information_and_inheritable_attributes_for_all_subclasses#:nodoc: @@ -1504,8 +1505,16 @@ module ActiveRecord #:nodoc: "(#{segments.join(') AND (')})" unless segments.empty? end - def arel_table(table = nil) - Relation.new(self, Arel::Table.new(table || table_name)) + def active_relation + @active_relation ||= Relation.new(self, active_relation_table) + end + + def active_relation_table(table_name_alias = nil) + Arel::Table.new(table_name, :as => table_name_alias) + end + + def active_relation_engine + @active_relation_engine ||= Arel::Sql::Engine.new(self) end private @@ -1561,7 +1570,7 @@ module ActiveRecord #:nodoc: def construct_finder_arel(options = {}, scope = scope(:find)) validate_find_options(options) - relation = arel_table. + relation = active_relation. joins(construct_join(options[:joins], scope)). where(construct_conditions(options[:conditions], scope)). select(options[:select] || (scope && scope[:select]) || default_select(options[:joins] || (scope && scope[:joins]))). @@ -1570,7 +1579,8 @@ module ActiveRecord #:nodoc: order(construct_order(options[:order], scope)). limit(construct_limit(options[:limit], scope)). offset(construct_offset(options[:offset], scope)). - from(options[:from]) + from(options[:from]). + includes( merge_includes(scope && scope[:include], options[:include])) lock = (scope && scope[:lock]) || options[:lock] relation = relation.lock if lock.present? @@ -1580,21 +1590,6 @@ module ActiveRecord #:nodoc: relation end - def construct_finder_arel_with_includes(options = {}) - relation = construct_finder_arel(options) - include_associations = merge_includes(scope(:find, :include), options[:include]) - - if include_associations.any? - if references_eager_loaded_tables?(options) - relation = relation.eager_load(include_associations) - else - relation = relation.preload(include_associations) - end - end - - relation - end - def construct_join(joins, scope) merged_joins = scope && scope[:joins] && joins ? merge_joins(scope[:joins], joins) : (joins || scope && scope[:joins]) case merged_joins @@ -1662,7 +1657,7 @@ module ActiveRecord #:nodoc: def build_association_joins(joins) join_dependency = ActiveRecord::Associations::ClassMethods::JoinDependency.new(self, joins, nil) - relation = arel_table.relation + relation = active_relation.relation join_dependency.join_associations.map { |association| if (association_relation = association.relation).is_a?(Array) [Arel::InnerJoin.new(relation, association_relation.first, association.association_join.first).joins(relation), @@ -1677,14 +1672,14 @@ module ActiveRecord #:nodoc: o.is_a?(Array) && o.all?{|obj| obj.is_a?(String)} end - def type_condition(table_alias=nil) - quoted_table_alias = self.connection.quote_table_name(table_alias || table_name) - quoted_inheritance_column = connection.quote_column_name(inheritance_column) - type_condition = subclasses.inject("#{quoted_table_alias}.#{quoted_inheritance_column} = '#{sti_name}' " ) do |condition, subclass| - condition << "OR #{quoted_table_alias}.#{quoted_inheritance_column} = '#{subclass.sti_name}' " - end + def type_condition(table_alias = nil) + table = Arel::Table.new(table_name, :engine => active_relation_engine, :as => table_alias) + + sti_column = table[inheritance_column] + condition = sti_column.eq(sti_name) + subclasses.each{|subclass| condition = condition.or(sti_column.eq(subclass.sti_name)) } - " (#{type_condition}) " + condition.to_sql end # Guesses the table name, but does not decorate it with prefix and suffix information. @@ -1713,7 +1708,7 @@ module ActiveRecord #:nodoc: super unless all_attributes_exists?(attribute_names) if match.finder? options = arguments.extract_options! - relation = options.any? ? construct_finder_arel_with_includes(options) : scoped + relation = options.any? ? construct_finder_arel(options) : scoped relation.send :find_by_attributes, match, attribute_names, *arguments elsif match.instantiator? scoped.send :find_or_instantiator_by_attributes, match, attribute_names, *arguments, &block @@ -1964,7 +1959,7 @@ module ActiveRecord #:nodoc: # ["name='%s' and group_id='%s'", "foo'bar", 4] returns "name='foo''bar' and group_id='4'" # { :name => "foo'bar", :group_id => 4 } returns "name='foo''bar' and group_id='4'" # "name='foo''bar' and group_id='4'" returns "name='foo''bar' and group_id='4'" - def sanitize_sql_for_conditions(condition, table_name = quoted_table_name) + def sanitize_sql_for_conditions(condition, table_name = self.table_name) return nil if condition.blank? case condition @@ -2035,30 +2030,12 @@ module ActiveRecord #:nodoc: # And for value objects on a composed_of relationship: # { :address => Address.new("123 abc st.", "chicago") } # # => "address_street='123 abc st.' and address_city='chicago'" - def sanitize_sql_hash_for_conditions(attrs, default_table_name = quoted_table_name) + def sanitize_sql_hash_for_conditions(attrs, default_table_name = self.table_name) attrs = expand_hash_conditions_for_aggregates(attrs) - conditions = attrs.map do |attr, value| - table_name = default_table_name - - unless value.is_a?(Hash) - attr = attr.to_s - - # Extract table name from qualified attribute names. - if attr.include?('.') - attr_table_name, attr = attr.split('.', 2) - attr_table_name = connection.quote_table_name(attr_table_name) - else - attr_table_name = table_name - end - - attribute_condition("#{attr_table_name}.#{connection.quote_column_name(attr)}", value) - else - sanitize_sql_hash_for_conditions(value, connection.quote_table_name(attr.to_s)) - end - end.join(' AND ') - - replace_bind_variables(conditions, expand_range_bind_variables(attrs.values)) + table = Arel::Table.new(default_table_name, active_relation_engine) + builder = PredicateBuilder.new(active_relation_engine) + builder.build_from_hash(attrs, table).map(&:to_sql).join(' AND ') end alias_method :sanitize_sql_hash, :sanitize_sql_hash_for_conditions @@ -2323,7 +2300,7 @@ module ActiveRecord #:nodoc: # be made (since they can't be persisted). def destroy unless new_record? - self.class.arel_table.where(self.class.arel_table[self.class.primary_key].eq(id)).delete + self.class.active_relation.where(self.class.active_relation[self.class.primary_key].eq(id)).delete_all end @destroyed = true @@ -2610,7 +2587,7 @@ module ActiveRecord #:nodoc: def update(attribute_names = @attributes.keys) attributes_with_values = arel_attributes_values(false, false, attribute_names) return 0 if attributes_with_values.empty? - self.class.arel_table.where(self.class.arel_table[self.class.primary_key].eq(id)).update(attributes_with_values) + self.class.active_relation.where(self.class.active_relation[self.class.primary_key].eq(id)).update(attributes_with_values) end # Creates a record with values matching those of the instance attributes @@ -2623,9 +2600,9 @@ module ActiveRecord #:nodoc: attributes_values = arel_attributes_values new_id = if attributes_values.empty? - self.class.arel_table.insert connection.empty_insert_statement_value + self.class.active_relation.insert connection.empty_insert_statement_value else - self.class.arel_table.insert attributes_values + self.class.active_relation.insert attributes_values end self.id ||= new_id @@ -2720,7 +2697,7 @@ module ActiveRecord #:nodoc: if value && ((self.class.serialized_attributes.has_key?(name) && (value.acts_like?(:date) || value.acts_like?(:time))) || value.is_a?(Hash) || value.is_a?(Array)) value = value.to_yaml end - attrs[self.class.arel_table[name]] = value + attrs[self.class.active_relation[name]] = value end end end diff --git a/activerecord/lib/active_record/calculations.rb b/activerecord/lib/active_record/calculations.rb index d51d9f2159..20d287faeb 100644 --- a/activerecord/lib/active_record/calculations.rb +++ b/activerecord/lib/active_record/calculations.rb @@ -162,7 +162,7 @@ module ActiveRecord join_dependency = ActiveRecord::Associations::ClassMethods::JoinDependency.new(self, includes, construct_join(options[:joins], scope)) construct_calculation_arel_with_included_associations(options, join_dependency) else - arel_table. + active_relation. joins(construct_join(options[:joins], scope)). from((scope && scope[:from]) || options[:from]). where(construct_conditions(options[:conditions], scope)). @@ -178,7 +178,7 @@ module ActiveRecord def construct_calculation_arel_with_included_associations(options, join_dependency) scope = scope(:find) - relation = arel_table + relation = active_relation for association in join_dependency.join_associations relation = association.join_relation(relation) diff --git a/activerecord/lib/active_record/locale/en.yml b/activerecord/lib/active_record/locale/en.yml index 092f5f0023..e33d389f8c 100644 --- a/activerecord/lib/active_record/locale/en.yml +++ b/activerecord/lib/active_record/locale/en.yml @@ -1,6 +1,9 @@ en: activerecord: errors: + # model.errors.full_messages format. + format: "{{attribute}} {{message}}" + # The values :model, :attribute and :value are always available for interpolation # The value :count is available when applicable. Can be used for pluralization. messages: diff --git a/activerecord/lib/active_record/locking/optimistic.rb b/activerecord/lib/active_record/locking/optimistic.rb index 986bc7009b..f9e538c586 100644 --- a/activerecord/lib/active_record/locking/optimistic.rb +++ b/activerecord/lib/active_record/locking/optimistic.rb @@ -78,11 +78,11 @@ module ActiveRecord attribute_names.uniq! begin - arel_table = self.class.arel_table(self.class.table_name) + relation = self.class.active_relation - affected_rows = arel_table.where( - arel_table[self.class.primary_key].eq(quoted_id).and( - arel_table[self.class.locking_column].eq(quote_value(previous_value)) + affected_rows = relation.where( + relation[self.class.primary_key].eq(quoted_id).and( + relation[self.class.locking_column].eq(quote_value(previous_value)) ) ).update(arel_attributes_values(false, false, attribute_names)) diff --git a/activerecord/lib/active_record/named_scope.rb b/activerecord/lib/active_record/named_scope.rb index a6336e762a..f63b249241 100644 --- a/activerecord/lib/active_record/named_scope.rb +++ b/activerecord/lib/active_record/named_scope.rb @@ -27,9 +27,9 @@ module ActiveRecord Scope.new(self, options, &block) else unless scoped?(:find) - finder_needs_type_condition? ? arel_table.where(type_condition) : arel_table + finder_needs_type_condition? ? active_relation.where(type_condition) : active_relation.spawn else - construct_finder_arel_with_includes + construct_finder_arel end end end diff --git a/activerecord/lib/active_record/rails.rb b/activerecord/lib/active_record/railtie.rb index a13bd2a5da..657ee738c0 100644 --- a/activerecord/lib/active_record/rails.rb +++ b/activerecord/lib/active_record/railtie.rb @@ -2,10 +2,12 @@ # rails, so let's make sure that it gets required before # here. This is needed for correctly setting up the middleware. # In the future, this might become an optional require. -require "action_controller/rails" +require "active_record" +require "action_controller/railtie" +require "rails" module ActiveRecord - class Plugin < Rails::Plugin + class Railtie < Rails::Railtie plugin_name :active_record rake_tasks do diff --git a/activerecord/lib/active_record/relation.rb b/activerecord/lib/active_record/relation.rb index ae03e1d7e9..114095b7ef 100644 --- a/activerecord/lib/active_record/relation.rb +++ b/activerecord/lib/active_record/relation.rb @@ -2,32 +2,60 @@ module ActiveRecord class Relation include QueryMethods, FinderMethods, CalculationMethods - delegate :to_sql, :to => :relation delegate :length, :collect, :map, :each, :all?, :to => :to_a - attr_reader :relation, :klass, :preload_associations, :eager_load_associations - attr_writer :readonly, :preload_associations, :eager_load_associations + attr_reader :relation, :klass + attr_writer :readonly, :table + attr_accessor :preload_associations, :eager_load_associations, :include_associations def initialize(klass, relation) @klass, @relation = klass, relation @preload_associations = [] @eager_load_associations = [] + @include_associations = [] @loaded, @readonly = false end + def new(*args, &block) + with_create_scope { @klass.new(*args, &block) } + end + + def create(*args, &block) + with_create_scope { @klass.create(*args, &block) } + end + + def create!(*args, &block) + with_create_scope { @klass.create!(*args, &block) } + end + def merge(r) raise ArgumentError, "Cannot merge a #{r.klass.name} relation with #{@klass.name} relation" if r.klass != @klass - joins(r.relation.joins(r.relation)). - group(r.send(:group_clauses).join(', ')). - order(r.send(:order_clauses).join(', ')). - where(r.send(:where_clause)). - limit(r.taken). - offset(r.skipped). - select(r.send(:select_clauses).join(', ')). - eager_load(r.eager_load_associations). - preload(r.preload_associations). - from(r.send(:sources).present? ? r.send(:from_clauses) : nil) + merged_relation = spawn(table).eager_load(r.eager_load_associations).preload(r.preload_associations).includes(r.include_associations) + merged_relation.readonly = r.readonly + + [self.relation, r.relation].each do |arel| + merged_relation = merged_relation. + joins(arel.joins(arel)). + group(arel.groupings). + order(arel.send(:order_clauses).join(', ')). + limit(arel.taken). + offset(arel.skipped). + select(arel.send(:select_clauses)). + from(arel.sources) + end + + merged_wheres = @relation.wheres + + r.wheres.each do |w| + if w.is_a?(Arel::Predicates::Equality) + merged_wheres = merged_wheres.reject {|p| p.is_a?(Arel::Predicates::Equality) && p.operand1.name == w.operand1.name } + end + + merged_wheres << w + end + + merged_relation.where(*merged_wheres) end alias :& :merge @@ -47,7 +75,9 @@ module ActiveRecord def to_a return @records if loaded? - @records = if @eager_load_associations.any? + find_with_associations = @eager_load_associations.any? || references_eager_loaded_tables? + + @records = if find_with_associations begin @klass.send(:find_with_associations, { :select => @relation.send(:select_clauses).join(', '), @@ -59,7 +89,7 @@ module ActiveRecord :offset => @relation.skipped, :from => (@relation.send(:from_clauses) if @relation.send(:sources).present?) }, - ActiveRecord::Associations::ClassMethods::JoinDependency.new(@klass, @eager_load_associations, nil)) + ActiveRecord::Associations::ClassMethods::JoinDependency.new(@klass, @eager_load_associations + @include_associations, nil)) rescue ThrowResult [] end @@ -67,7 +97,10 @@ module ActiveRecord @klass.find_by_sql(@relation.to_sql) end - @preload_associations.each {|associations| @klass.send(:preload_associations, @records, associations) } + preload = @preload_associations + preload += @include_associations unless find_with_associations + preload.each {|associations| @klass.send(:preload_associations, @records, associations) } + @records.each { |record| record.readonly! } if @readonly @loaded = true @@ -109,6 +142,10 @@ module ActiveRecord @relation.delete.tap { reset } end + def delete(id_or_array) + where(@klass.primary_key => id_or_array).delete_all + end + def loaded? @loaded end @@ -119,19 +156,33 @@ module ActiveRecord end def reset - @first = @last = nil + @first = @last = @create_scope = @to_sql = nil @records = [] self end def spawn(relation = @relation) - relation = self.class.new(@klass, relation) + relation = Relation.new(@klass, relation) relation.readonly = @readonly relation.preload_associations = @preload_associations relation.eager_load_associations = @eager_load_associations + relation.include_associations = @include_associations + relation.table = table relation end + def table + @table ||= Arel::Table.new(@klass.table_name, Arel::Sql::Engine.new(@klass)) + end + + def primary_key + @primary_key ||= table[@klass.primary_key] + end + + def to_sql + @to_sql ||= @relation.to_sql + end + protected def method_missing(method, *args, &block) @@ -153,9 +204,30 @@ module ActiveRecord end end - def where_clause(join_string = "\n\tAND ") + def with_create_scope + @klass.send(:with_scope, :create => create_scope) { yield } + end + + def create_scope + @create_scope ||= wheres.inject({}) do |hash, where| + hash[where.operand1.name] = where.operand2.value if where.is_a?(Arel::Predicates::Equality) + hash + end + end + + def where_clause(join_string = " AND ") @relation.send(:where_clauses).join(join_string) end + def references_eager_loaded_tables? + joined_tables = (tables_in_string(@relation.joins(relation)) + [table.name, table.table_alias]).compact.uniq + (tables_in_string(to_sql) - joined_tables).any? + end + + def tables_in_string(string) + return [] if string.blank? + string.scan(/([a-zA-Z_][\.\w]+).?\./).flatten.uniq + end + end end diff --git a/activerecord/lib/active_record/relation/calculation_methods.rb b/activerecord/lib/active_record/relation/calculation_methods.rb index a925a99464..5246c7bc5d 100644 --- a/activerecord/lib/active_record/relation/calculation_methods.rb +++ b/activerecord/lib/active_record/relation/calculation_methods.rb @@ -53,7 +53,7 @@ module ActiveRecord def execute_simple_calculation(operation, column_name, distinct) #:nodoc: column = if @klass.column_names.include?(column_name.to_s) - Arel::Attribute.new(@klass.arel_table, column_name) + Arel::Attribute.new(@klass.active_relation, column_name) else Arel::SqlLiteral.new(column_name == :all ? "*" : column_name.to_s) end @@ -77,7 +77,7 @@ module ActiveRecord select_statement = if operation == 'count' && column_name == :all "COUNT(*) AS count_all" else - Arel::Attribute.new(@klass.arel_table, column_name).send(operation).as(aggregate_alias).to_sql + Arel::Attribute.new(@klass.active_relation, column_name).send(operation).as(aggregate_alias).to_sql end select_statement << ", #{group_field} AS #{group_alias}" diff --git a/activerecord/lib/active_record/relation/finder_methods.rb b/activerecord/lib/active_record/relation/finder_methods.rb index 7a1d6fc538..c3e5f27838 100644 --- a/activerecord/lib/active_record/relation/finder_methods.rb +++ b/activerecord/lib/active_record/relation/finder_methods.rb @@ -21,8 +21,8 @@ module ActiveRecord end def exists?(id = nil) - relation = select("#{@klass.quoted_table_name}.#{@klass.primary_key}").limit(1) - relation = relation.where(@klass.primary_key => id) if id + relation = select(primary_key).limit(1) + relation = relation.where(primary_key.eq(id)) if id relation.first ? true : false end @@ -78,7 +78,7 @@ module ActiveRecord end def find_one(id) - record = where(@klass.primary_key => id).first + record = where(primary_key.eq(id)).first unless record conditions = where_clause(', ') @@ -90,7 +90,7 @@ module ActiveRecord end def find_some(ids) - result = where(@klass.primary_key => ids).all + result = where(primary_key.in(ids)).all expected_size = if @relation.taken && ids.size > @relation.taken diff --git a/activerecord/lib/active_record/relation/predicate_builder.rb b/activerecord/lib/active_record/relation/predicate_builder.rb new file mode 100644 index 0000000000..6b7d941350 --- /dev/null +++ b/activerecord/lib/active_record/relation/predicate_builder.rb @@ -0,0 +1,45 @@ +module ActiveRecord + class PredicateBuilder + + def initialize(engine) + @engine = engine + end + + def build_from_hash(attributes, default_table) + predicates = attributes.map do |column, value| + table = default_table + + if value.is_a?(Hash) + table = Arel::Table.new(column, :engine => @engine) + build_from_hash(value, table) + else + column = column.to_s + + if column.include?('.') + table_name, column = column.split('.', 2) + table = Arel::Table.new(table_name, :engine => @engine) + end + + attribute = table[column] || Arel::Attribute.new(table, column.to_sym) + + case value + when Array, ActiveRecord::Associations::AssociationCollection, ActiveRecord::NamedScope::Scope + attribute.in(value) + when Range + # TODO : Arel should handle ranges with excluded end. + if value.exclude_end? + [attribute.gteq(value.begin), attribute.lt(value.end)] + else + attribute.in(value) + end + else + attribute.eq(value) + end + end + end + + predicates.flatten + end + + end +end diff --git a/activerecord/lib/active_record/relation/query_methods.rb b/activerecord/lib/active_record/relation/query_methods.rb index 631c80da25..cf2cc7ba70 100644 --- a/activerecord/lib/active_record/relation/query_methods.rb +++ b/activerecord/lib/active_record/relation/query_methods.rb @@ -5,6 +5,10 @@ module ActiveRecord spawn.tap {|r| r.preload_associations += Array.wrap(associations) } end + def includes(*associations) + spawn.tap {|r| r.include_associations += Array.wrap(associations) } + end + def eager_load(*associations) spawn.tap {|r| r.eager_load_associations += Array.wrap(associations) } end @@ -104,14 +108,19 @@ module ActiveRecord def where(*args) return spawn if args.blank? - if [String, Hash, Array].include?(args.first.class) - conditions = @klass.send(:merge_conditions, args.size > 1 ? Array.wrap(args) : args.first) - conditions = Arel::SqlLiteral.new(conditions) if conditions + builder = PredicateBuilder.new(Arel::Sql::Engine.new(@klass)) + + conditions = if [String, Array].include?(args.first.class) + merged = @klass.send(:merge_conditions, args.size > 1 ? Array.wrap(args) : args.first) + Arel::SqlLiteral.new(merged) if merged + elsif args.first.is_a?(Hash) + attributes = @klass.send(:expand_hash_conditions_for_aggregates, args.first) + builder.build_from_hash(attributes, table) else - conditions = args.first + args.first end - spawn(@relation.where(conditions)) + conditions.is_a?(String) ? spawn(@relation.where(conditions)) : spawn(@relation.where(*conditions)) end private diff --git a/activerecord/lib/active_record/validations/uniqueness.rb b/activerecord/lib/active_record/validations/uniqueness.rb index ffbe1b5c40..7efd312357 100644 --- a/activerecord/lib/active_record/validations/uniqueness.rb +++ b/activerecord/lib/active_record/validations/uniqueness.rb @@ -8,24 +8,25 @@ module ActiveRecord def validate_each(record, attribute, value) finder_class = find_finder_class_for(record) + table = finder_class.active_relation + table_name = record.class.quoted_table_name sql, params = mount_sql_and_params(finder_class, table_name, attribute, value) + relation = table.where(sql, *params) + Array(options[:scope]).each do |scope_item| scope_value = record.send(scope_item) - sql << " AND " << record.class.send(:attribute_condition, "#{table_name}.#{scope_item}", scope_value) - params << scope_value + relation = relation.where(scope_item => scope_value) end unless record.new_record? - sql << " AND #{record.class.quoted_table_name}.#{record.class.primary_key} <> ?" - params << record.send(:id) + # TODO : This should be in Arel + relation = relation.where("#{record.class.quoted_table_name}.#{record.class.primary_key} <> ?", record.send(:id)) end - finder_class.send(:with_exclusive_scope) do - if finder_class.exists?([sql, *params]) - record.errors.add(attribute, :taken, :default => options[:message], :value => value) - end + if relation.exists? + record.errors.add(attribute, :taken, :default => options[:message], :value => value) end end |