diff options
Diffstat (limited to 'activerecord/lib')
38 files changed, 809 insertions, 802 deletions
diff --git a/activerecord/lib/active_record.rb b/activerecord/lib/active_record.rb index 728dec8925..d5b6d40514 100644 --- a/activerecord/lib/active_record.rb +++ b/activerecord/lib/active_record.rb @@ -134,6 +134,9 @@ module ActiveRecord autoload :AbstractAdapter end end + + autoload :TestCase + autoload :TestFixtures, 'active_record/fixtures' end Arel::Table.engine = Arel::Sql::Engine.new(ActiveRecord::Base) diff --git a/activerecord/lib/active_record/association_preload.rb b/activerecord/lib/active_record/association_preload.rb index 9f7b2a60b2..a43c4d09d6 100644 --- a/activerecord/lib/active_record/association_preload.rb +++ b/activerecord/lib/active_record/association_preload.rb @@ -188,11 +188,11 @@ module ActiveRecord conditions << append_conditions(reflection, preload_options) associated_records = reflection.klass.with_exclusive_scope do - reflection.klass.find(:all, :conditions => [conditions, ids], - :include => options[:include], - :joins => "INNER JOIN #{connection.quote_table_name options[:join_table]} t0 ON #{reflection.klass.quoted_table_name}.#{reflection.klass.primary_key} = t0.#{reflection.association_foreign_key}", - :select => "#{options[:select] || table_name+'.*'}, t0.#{reflection.primary_key_name} as the_parent_record_id", - :order => options[:order]) + reflection.klass.where([conditions, ids]). + includes(options[:include]). + joins("INNER JOIN #{connection.quote_table_name options[:join_table]} t0 ON #{reflection.klass.quoted_table_name}.#{reflection.klass.primary_key} = t0.#{reflection.association_foreign_key}"). + select("#{options[:select] || table_name+'.*'}, t0.#{reflection.primary_key_name} as the_parent_record_id"). + order(options[:order]).to_a end set_association_collection_records(id_to_record_map, reflection.name, associated_records, 'the_parent_record_id') end @@ -327,6 +327,7 @@ module ActiveRecord table_name = klass.quoted_table_name primary_key = klass.primary_key column_type = klass.columns.detect{|c| c.name == primary_key}.type + ids = id_map.keys.map do |id| if column_type == :integer id.to_i @@ -336,15 +337,14 @@ module ActiveRecord id end end + conditions = "#{table_name}.#{connection.quote_column_name(primary_key)} #{in_or_equals_for_ids(ids)}" conditions << append_conditions(reflection, preload_options) + associated_records = klass.with_exclusive_scope do - klass.find(:all, :conditions => [conditions, ids], - :include => options[:include], - :select => options[:select], - :joins => options[:joins], - :order => options[:order]) + klass.where([conditions, ids]).apply_finder_options(options.slice(:include, :select, :joins, :order)).to_a end + set_association_single_records(id_map, reflection.name, associated_records, primary_key) end end @@ -363,13 +363,12 @@ module ActiveRecord conditions << append_conditions(reflection, preload_options) reflection.klass.with_exclusive_scope do - reflection.klass.find(:all, - :select => (preload_options[:select] || options[:select] || "#{table_name}.*"), - :include => preload_options[:include] || options[:include], - :conditions => [conditions, ids], - :joins => options[:joins], - :group => preload_options[:group] || options[:group], - :order => preload_options[:order] || options[:order]) + reflection.klass.select(preload_options[:select] || options[:select] || "#{table_name}.*"). + includes(preload_options[:include] || options[:include]). + where([conditions, ids]). + joins(options[:joins]). + group(preload_options[:group] || options[:group]). + order(preload_options[:order] || options[:order]) end end diff --git a/activerecord/lib/active_record/associations.rb b/activerecord/lib/active_record/associations.rb index 149a11eb47..468a6cd9f8 100755 --- a/activerecord/lib/active_record/associations.rb +++ b/activerecord/lib/active_record/associations.rb @@ -1701,24 +1701,30 @@ module ActiveRecord end def construct_finder_arel_with_included_associations(options, join_dependency) - scope = scope(:find) - - relation = active_relation + relation = unscoped for association in join_dependency.join_associations relation = association.join_relation(relation) end - relation = relation.joins(construct_join(options[:joins], scope)). + relation = relation.joins(options[:joins]). select(column_aliases(join_dependency)). - group(options[:group] || (scope && scope[:group])). - having(options[:having] || (scope && scope[:having])). - order(construct_order(options[:order], scope)). - where(construct_conditions(options[:conditions], scope)). - from((scope && scope[:from]) || options[:from]) + group(options[:group]). + having(options[:having]). + order(options[:order]). + where(options[:conditions]). + from(options[:from]) + + scoped_relation = current_scoped_methods + scoped_relation_limit = scoped_relation.taken if scoped_relation + + relation = current_scoped_methods.except(:limit).merge(relation) if current_scoped_methods - relation = relation.where(construct_arel_limited_ids_condition(options, join_dependency)) if !using_limitable_reflections?(join_dependency.reflections) && ((scope && scope[:limit]) || options[:limit]) - relation = relation.limit(construct_limit(options[:limit], scope)) if using_limitable_reflections?(join_dependency.reflections) + if !using_limitable_reflections?(join_dependency.reflections) && ((scoped_relation && scoped_relation.taken) || options[:limit]) + relation = relation.where(construct_arel_limited_ids_condition(options, join_dependency)) + end + + relation = relation.limit(options[:limit] || scoped_relation_limit) if using_limitable_reflections?(join_dependency.reflections) relation end @@ -1746,29 +1752,29 @@ module ActiveRecord end def construct_finder_sql_for_association_limiting(options, join_dependency) - scope = scope(:find) - - relation = active_relation + relation = unscoped for association in join_dependency.join_associations relation = association.join_relation(relation) end - relation = relation.joins(construct_join(options[:joins], scope)). - where(construct_conditions(options[:conditions], scope)). - group(options[:group] || (scope && scope[:group])). - having(options[:having] || (scope && scope[:having])). - 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 = relation.joins(options[:joins]). + where(options[:conditions]). + group(options[:group]). + having(options[:having]). + order(options[:order]). + limit(options[:limit]). + offset(options[:offset]). + from(options[:from]) + + relation = current_scoped_methods.except(:select, :includes, :eager_load).merge(relation) if current_scoped_methods + relation = relation.select(connection.distinct("#{connection.quote_table_name table_name}.#{primary_key}", options[:order])) relation.to_sql end def using_limitable_reflections?(reflections) - reflections.reject { |r| [ :belongs_to, :has_one ].include?(r.macro) }.length.zero? + reflections.collect(&:collection?).length.zero? end def column_aliases(join_dependency) @@ -1841,7 +1847,7 @@ module ActiveRecord case associations when Symbol, String reflection = base.reflections[associations] - if reflection && [:has_many, :has_and_belongs_to_many].include?(reflection.macro) + if reflection && reflection.collection? records.each { |record| record.send(reflection.name).target.uniq! } end when Array @@ -1851,12 +1857,11 @@ module ActiveRecord when Hash associations.keys.each do |name| reflection = base.reflections[name] - is_collection = [:has_many, :has_and_belongs_to_many].include?(reflection.macro) parent_records = [] records.each do |record| if descendant = record.send(reflection.name) - if is_collection + if reflection.collection? parent_records.concat descendant.target.uniq else parent_records << descendant @@ -1954,7 +1959,7 @@ module ActiveRecord class JoinBase # :nodoc: attr_reader :active_record, :table_joins - delegate :table_name, :column_names, :primary_key, :reflections, :sanitize_sql, :to => :active_record + delegate :table_name, :column_names, :primary_key, :reflections, :sanitize_sql, :arel_engine, :to => :active_record def initialize(active_record, joins = nil) @active_record = active_record @@ -2029,140 +2034,108 @@ module ActiveRecord def association_join return @join if @join - connection = reflection.active_record.connection + + aliased_table = Arel::Table.new(table_name, :as => @aliased_table_name, :engine => arel_engine) + parent_table = Arel::Table.new(parent.table_name, :as => parent.aliased_table_name, :engine => arel_engine) + @join = case reflection.macro - when :has_and_belongs_to_many - ["%s.%s = %s.%s " % [ - connection.quote_table_name(aliased_join_table_name), - options[:foreign_key] || reflection.active_record.to_s.foreign_key, - connection.quote_table_name(parent.aliased_table_name), - reflection.active_record.primary_key], - "%s.%s = %s.%s " % [ - connection.quote_table_name(aliased_table_name), - klass.primary_key, - connection.quote_table_name(aliased_join_table_name), - options[:association_foreign_key] || klass.to_s.foreign_key - ] - ] - when :has_many, :has_one - if reflection.options[:through] - jt_foreign_key = jt_as_extra = jt_source_extra = jt_sti_extra = nil - first_key = second_key = as_extra = nil - - if through_reflection.options[:as] # has_many :through against a polymorphic join - jt_foreign_key = through_reflection.options[:as].to_s + '_id' - jt_as_extra = " AND %s.%s = %s" % [ - connection.quote_table_name(aliased_join_table_name), - connection.quote_column_name(through_reflection.options[:as].to_s + '_type'), - klass.quote_value(parent.active_record.base_class.name) - ] + when :has_and_belongs_to_many + join_table = Arel::Table.new(options[:join_table], :as => aliased_join_table_name, :engine => arel_engine) + fk = options[:foreign_key] || reflection.active_record.to_s.foreign_key + klass_fk = options[:association_foreign_key] || klass.to_s.foreign_key + + [ + join_table[fk].eq(parent_table[reflection.active_record.primary_key]), + aliased_table[klass.primary_key].eq(join_table[klass_fk]) + ] + when :has_many, :has_one + if reflection.options[:through] + join_table = Arel::Table.new(through_reflection.klass.table_name, :as => aliased_join_table_name, :engine => arel_engine) + jt_foreign_key = jt_as_extra = jt_source_extra = jt_sti_extra = nil + first_key = second_key = as_extra = nil + + if through_reflection.options[:as] # has_many :through against a polymorphic join + jt_foreign_key = through_reflection.options[:as].to_s + '_id' + jt_as_extra = join_table[through_reflection.options[:as].to_s + '_type'].eq(parent.active_record.base_class.name) + else + jt_foreign_key = through_reflection.primary_key_name + end + + case source_reflection.macro + when :has_many + if source_reflection.options[:as] + first_key = "#{source_reflection.options[:as]}_id" + second_key = options[:foreign_key] || primary_key + as_extra = aliased_table["#{source_reflection.options[:as]}_type"].eq(source_reflection.active_record.base_class.name) else - jt_foreign_key = through_reflection.primary_key_name + first_key = through_reflection.klass.base_class.to_s.foreign_key + second_key = options[:foreign_key] || primary_key end - case source_reflection.macro - when :has_many - if source_reflection.options[:as] - first_key = "#{source_reflection.options[:as]}_id" - second_key = options[:foreign_key] || primary_key - as_extra = " AND %s.%s = %s" % [ - connection.quote_table_name(aliased_table_name), - connection.quote_column_name("#{source_reflection.options[:as]}_type"), - klass.quote_value(source_reflection.active_record.base_class.name) - ] - else - first_key = through_reflection.klass.base_class.to_s.foreign_key - second_key = options[:foreign_key] || primary_key - end - - unless through_reflection.klass.descends_from_active_record? - jt_sti_extra = " AND %s.%s = %s" % [ - connection.quote_table_name(aliased_join_table_name), - connection.quote_column_name(through_reflection.active_record.inheritance_column), - through_reflection.klass.quote_value(through_reflection.klass.sti_name)] - end - when :belongs_to - first_key = primary_key - if reflection.options[:source_type] - second_key = source_reflection.association_foreign_key - jt_source_extra = " AND %s.%s = %s" % [ - connection.quote_table_name(aliased_join_table_name), - connection.quote_column_name(reflection.source_reflection.options[:foreign_type]), - klass.quote_value(reflection.options[:source_type]) - ] - else - second_key = source_reflection.primary_key_name - end + unless through_reflection.klass.descends_from_active_record? + jt_sti_extra = join_table[through_reflection.active_record.inheritance_column].eq(through_reflection.klass.sti_name) + end + when :belongs_to + first_key = primary_key + if reflection.options[:source_type] + second_key = source_reflection.association_foreign_key + jt_source_extra = join_table[reflection.source_reflection.options[:foreign_type]].eq(reflection.options[:source_type]) + else + second_key = source_reflection.primary_key_name end - - ["(%s.%s = %s.%s%s%s%s) " % [ - connection.quote_table_name(parent.aliased_table_name), - connection.quote_column_name(parent.primary_key), - connection.quote_table_name(aliased_join_table_name), - connection.quote_column_name(jt_foreign_key), - jt_as_extra, jt_source_extra, jt_sti_extra], - "(%s.%s = %s.%s%s) " % [ - connection.quote_table_name(aliased_table_name), - connection.quote_column_name(first_key), - connection.quote_table_name(aliased_join_table_name), - connection.quote_column_name(second_key), - as_extra] - ] - - elsif reflection.options[:as] - "%s.%s = %s.%s AND %s.%s = %s" % [ - connection.quote_table_name(aliased_table_name), - "#{reflection.options[:as]}_id", - connection.quote_table_name(parent.aliased_table_name), - parent.primary_key, - connection.quote_table_name(aliased_table_name), - "#{reflection.options[:as]}_type", - klass.quote_value(parent.active_record.base_class.name) - ] - else - foreign_key = options[:foreign_key] || reflection.active_record.name.foreign_key - "%s.%s = %s.%s " % [ - aliased_table_name, - foreign_key, - parent.aliased_table_name, - reflection.options[:primary_key] || parent.primary_key - ] end - when :belongs_to - "%s.%s = %s.%s " % [ - connection.quote_table_name(aliased_table_name), - reflection.klass.primary_key, - connection.quote_table_name(parent.aliased_table_name), - options[:foreign_key] || reflection.primary_key_name + + [ + [parent_table[parent.primary_key].eq(join_table[jt_foreign_key]), jt_as_extra, jt_source_extra, jt_sti_extra].reject{|x| x.blank? }, + aliased_table[first_key].eq(join_table[second_key]) ] + elsif reflection.options[:as] + id_rel = aliased_table["#{reflection.options[:as]}_id"].eq(parent_table[parent.primary_key]) + type_rel = aliased_table["#{reflection.options[:as]}_type"].eq(parent.active_record.base_class.name) + [id_rel, type_rel] + else + foreign_key = options[:foreign_key] || reflection.active_record.name.foreign_key + [aliased_table[foreign_key].eq(parent_table[reflection.options[:primary_key] || parent.primary_key])] + end + when :belongs_to + [aliased_table[reflection.klass.primary_key].eq(parent_table[options[:foreign_key] || reflection.primary_key_name])] + end + + unless klass.descends_from_active_record? + sti_column = aliased_table[klass.inheritance_column] + sti_condition = sti_column.eq(klass.sti_name) + klass.send(:subclasses).each {|subclass| sti_condition = sti_condition.or(sti_column.eq(subclass.sti_name)) } + + @join << sti_condition end - @join << %(AND %s) % [ - klass.send(:type_condition, aliased_table_name)] unless klass.descends_from_active_record? [through_reflection, reflection].each do |ref| - @join << "AND #{interpolate_sql(sanitize_sql(ref.options[:conditions], aliased_table_name))} " if ref && ref.options[:conditions] + if ref && ref.options[:conditions] + @join << interpolate_sql(sanitize_sql(ref.options[:conditions], aliased_table_name)) + end end @join end def relation + aliased = Arel::Table.new(table_name, :as => @aliased_table_name, :engine => arel_engine) + if reflection.macro == :has_and_belongs_to_many - [Arel::Table.new(table_alias_for(options[:join_table], aliased_join_table_name)), Arel::Table.new(table_name_and_alias)] + [Arel::Table.new(options[:join_table], :as => aliased_join_table_name, :engine => arel_engine), aliased] elsif reflection.options[:through] - [Arel::Table.new(table_alias_for(through_reflection.klass.table_name, aliased_join_table_name)), Arel::Table.new(table_name_and_alias)] + [Arel::Table.new(through_reflection.klass.table_name, :as => aliased_join_table_name, :engine => arel_engine), aliased] else - Arel::Table.new(table_name_and_alias) + aliased end end def join_relation(joining_relation, join = nil) if (relations = relation).is_a?(Array) - joining_relation. - joins(relations.first, Arel::OuterJoin).on(association_join.first). - joins(relations.last, Arel::OuterJoin).on(association_join.last) + joining_relation.joins(Relation::JoinOperation.new(relations.first, Arel::OuterJoin, association_join.first)). + joins(Relation::JoinOperation.new(relations.last, Arel::OuterJoin, association_join.last)) else - joining_relation.joins(relations, Arel::OuterJoin).on(association_join) + joining_relation.joins(Relation::JoinOperation.new(relations, Arel::OuterJoin, association_join)) end end diff --git a/activerecord/lib/active_record/associations/association_collection.rb b/activerecord/lib/active_record/associations/association_collection.rb index 358db6df1d..64dd5cf629 100644 --- a/activerecord/lib/active_record/associations/association_collection.rb +++ b/activerecord/lib/active_record/associations/association_collection.rb @@ -58,11 +58,14 @@ module ActiveRecord find_scope = construct_scope[:find].slice(:conditions, :order) with_scope(:find => find_scope) do - relation = @reflection.klass.send(:construct_finder_arel, options) + relation = @reflection.klass.send(:construct_finder_arel, options, @reflection.klass.send(:current_scoped_methods)) case args.first - when :first, :last, :all + when :first, :last relation.send(args.first) + when :all + records = relation.all + @reflection.options[:uniq] ? uniq(records) : records else relation.find(*args) end @@ -402,7 +405,7 @@ module ActiveRecord end elsif @reflection.klass.scopes.include?(method) @reflection.klass.scopes[method].call(self, *args) - else + else with_scope(construct_scope) do if block_given? @reflection.klass.send(method, *args) { |*block_args| yield(*block_args) } diff --git a/activerecord/lib/active_record/associations/has_many_through_association.rb b/activerecord/lib/active_record/associations/has_many_through_association.rb index 214ce5959a..387b85aacd 100644 --- a/activerecord/lib/active_record/associations/has_many_through_association.rb +++ b/activerecord/lib/active_record/associations/has_many_through_association.rb @@ -51,8 +51,6 @@ module ActiveRecord end def construct_find_options!(options) - options[:select] = construct_select(options[:select]) - options[:from] ||= construct_from options[:joins] = construct_joins(options[:joins]) options[:include] = @reflection.source_reflection.options[:include] if options[:include].nil? && @reflection.source_reflection.options[:include] end diff --git a/activerecord/lib/active_record/associations/through_association_scope.rb b/activerecord/lib/active_record/associations/through_association_scope.rb index 1924156e2a..6f0f698f1e 100644 --- a/activerecord/lib/active_record/associations/through_association_scope.rb +++ b/activerecord/lib/active_record/associations/through_association_scope.rb @@ -6,8 +6,7 @@ module ActiveRecord def construct_scope { :create => construct_owner_attributes(@reflection), - :find => { :from => construct_from, - :conditions => construct_conditions, + :find => { :conditions => construct_conditions, :joins => construct_joins, :include => @reflection.options[:include] || @reflection.source_reflection.options[:include], :select => construct_select, @@ -145,7 +144,7 @@ module ActiveRecord end def build_sti_condition - @reflection.through_reflection.klass.send(:type_condition) + @reflection.through_reflection.klass.send(:type_condition).to_sql end alias_method :sql_conditions, :conditions diff --git a/activerecord/lib/active_record/autosave_association.rb b/activerecord/lib/active_record/autosave_association.rb index 98ab64537e..e178cb4ef2 100644 --- a/activerecord/lib/active_record/autosave_association.rb +++ b/activerecord/lib/active_record/autosave_association.rb @@ -158,47 +158,39 @@ module ActiveRecord # # For performance reasons, we don't check whether to validate at runtime, # but instead only define the method and callback when needed. However, - # this can change, for instance, when using nested attributes. Since we - # don't want the callbacks to get defined multiple times, there are - # guards that check if the save or validation methods have already been - # defined before actually defining them. + # this can change, for instance, when using nested attributes, which is + # called _after_ the association has been defined. Since we don't want + # the callbacks to get defined multiple times, there are guards that + # check if the save or validation methods have already been defined + # before actually defining them. def add_autosave_association_callbacks(reflection) - save_method = "autosave_associated_records_for_#{reflection.name}" - validation_method = "validate_associated_records_for_#{reflection.name}" - force_validation = (reflection.options[:validate] == true || reflection.options[:autosave] == true) + save_method = :"autosave_associated_records_for_#{reflection.name}" + validation_method = :"validate_associated_records_for_#{reflection.name}" + collection = reflection.collection? - case reflection.macro - when :has_many, :has_and_belongs_to_many - unless method_defined?(save_method) + unless method_defined?(save_method) + if collection before_save :before_save_collection_association define_method(save_method) { save_collection_association(reflection) } # Doesn't use after_save as that would save associations added in after_create/after_update twice after_create save_method after_update save_method - end - - if !method_defined?(validation_method) && - (force_validation || (reflection.macro == :has_many && reflection.options[:validate] != false)) - define_method(validation_method) { validate_collection_association(reflection) } - validate validation_method - end - else - unless method_defined?(save_method) - case reflection.macro - when :has_one + else + if reflection.macro == :has_one define_method(save_method) { save_has_one_association(reflection) } after_save save_method - when :belongs_to + else define_method(save_method) { save_belongs_to_association(reflection) } before_save save_method end end + end - if !method_defined?(validation_method) && force_validation - define_method(validation_method) { validate_single_association(reflection) } - validate validation_method - end + if reflection.validate? && !method_defined?(validation_method) + method = (collection ? :validate_collection_association : :validate_single_association) + define_method(validation_method) { send(method, reflection) } + validate validation_method end end end @@ -232,10 +224,10 @@ module ActiveRecord def associated_records_to_validate_or_save(association, new_record, autosave) if new_record association - elsif association.loaded? - autosave ? association : association.find_all { |record| record.new_record? } + elsif autosave + association.target.find_all { |record| record.new_record? || record.changed? || record.marked_for_destruction? } else - autosave ? association.target : association.target.find_all { |record| record.new_record? } + association.target.find_all { |record| record.new_record? } end end @@ -268,7 +260,8 @@ module ActiveRecord if reflection.options[:autosave] association.errors.each do |attribute, message| attribute = "#{reflection.name}.#{attribute}" - errors[attribute] << message if errors[attribute].empty? + errors[attribute] << message + errors[attribute].uniq! end else errors.add(reflection.name) @@ -304,13 +297,15 @@ module ActiveRecord association.destroy(record) elsif autosave != false && (@new_record_before_save || record.new_record?) if autosave - association.send(:insert_record, record, false, false) + saved = association.send(:insert_record, record, false, false) else association.send(:insert_record, record) end elsif autosave - record.save(false) + saved = record.save(false) end + + raise ActiveRecord::Rollback if saved == false end end @@ -337,7 +332,9 @@ module ActiveRecord key = reflection.options[:primary_key] ? send(reflection.options[:primary_key]) : id if autosave != false && (new_record? || association.new_record? || association[reflection.primary_key_name] != key || autosave) association[reflection.primary_key_name] = key - association.save(!autosave) + saved = association.save(!autosave) + raise ActiveRecord::Rollback if !saved && autosave + saved end end end @@ -358,7 +355,7 @@ module ActiveRecord if autosave && association.marked_for_destruction? association.destroy elsif autosave != false - association.save(!autosave) if association.new_record? || autosave + saved = association.save(!autosave) if association.new_record? || autosave if association.updated? association_id = association.send(reflection.options[:primary_key] || :id) @@ -368,6 +365,8 @@ module ActiveRecord self[reflection.options[:foreign_type]] = association.class.base_class.name.to_s end end + + saved if autosave end end end diff --git a/activerecord/lib/active_record/base.rb b/activerecord/lib/active_record/base.rb index 0ebcf4a3cc..4ee9887186 100755 --- a/activerecord/lib/active_record/base.rb +++ b/activerecord/lib/active_record/base.rb @@ -518,14 +518,6 @@ module ActiveRecord #:nodoc: ## # :singleton-method: - # Determines whether to use ANSI codes to colorize the logging statements committed by the connection adapter. These colors - # make it much easier to overview things during debugging (when used through a reader like +tail+ and on a black background), but - # may complicate matters if you use software like syslog. This is true, by default. - cattr_accessor :colorize_logging, :instance_writer => false - @@colorize_logging = true - - ## - # :singleton-method: # Determines whether to use Time.local (using :local) or Time.utc (using :utc) when pulling dates and times from the database. # This is set to :local by default. cattr_accessor :default_timezone, :instance_writer => false @@ -550,13 +542,20 @@ module ActiveRecord #:nodoc: # Determine whether to store the full constant name including namespace when using STI superclass_delegating_accessor :store_full_sti_class - self.store_full_sti_class = false + self.store_full_sti_class = true # Stores the default scope for the class class_inheritable_accessor :default_scoping, :instance_writer => false self.default_scoping = [] class << self # Class methods + def colorize_logging(*args) + ActiveSupport::Deprecation.warn "ActiveRecord::Base.colorize_logging and " << + "config.active_record.colorize_logging are deprecated. Please use " << + "Rails::Subscriber.colorize_logging or config.colorize_logging instead", caller + end + alias :colorize_logging= :colorize_logging + # Find operates with four different retrieval approaches: # # * Find by id - This can either be a specific id (1), a list of ids (1, 5, 6), or an array of ids ([5, 6, 10]). @@ -643,9 +642,8 @@ module ActiveRecord #:nodoc: # end def find(*args) options = args.extract_options! - set_readonly_option!(options) - relation = construct_finder_arel(options) + relation = construct_finder_arel(options, current_scoped_methods) case args.first when :first, :last, :all @@ -816,7 +814,7 @@ module ActiveRecord #:nodoc: # # Delete multiple rows # Todo.delete([2,3,4]) def delete(id_or_array) - active_relation.where(construct_conditions(nil, scope(:find))).delete(id_or_array) + scoped.delete(id_or_array) end # Destroy an object (or multiple objects) that has the given id, the object is instantiated first, @@ -871,20 +869,18 @@ module ActiveRecord #:nodoc: # # Update all books that match our conditions, but limit it to 5 ordered by date # Book.update_all "author = 'David'", "title LIKE '%Rails%'", :order => 'created_at', :limit => 5 def update_all(updates, conditions = nil, options = {}) - scope = scope(:find) - - relation = active_relation + relation = unscoped - if conditions = construct_conditions(conditions, scope) - relation = relation.where(Arel::SqlLiteral.new(conditions)) - end + relation = relation.where(conditions) if conditions + relation = relation.limit(options[:limit]) if options[:limit].present? + relation = relation.order(options[:order]) if options[:order].present? - relation = if options.has_key?(:limit) || (scope && scope[:limit]) + if current_scoped_methods && current_scoped_methods.limit_value.present? && current_scoped_methods.order_values.present? # Only take order from scope if limit is also provided by scope, this # is useful for updating a has_many association with a limit. - relation.order(construct_order(options[:order], scope)).limit(construct_limit(options[:limit], scope)) + relation = current_scoped_methods.merge(relation) if current_scoped_methods else - relation.order(options[:order]) + relation = current_scoped_methods.except(:limit, :order).merge(relation) if current_scoped_methods end relation.update(sanitize_sql_for_assignment(updates)) @@ -938,7 +934,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) - active_relation.where(construct_conditions(conditions, scope(:find))).delete_all + where(conditions).delete_all end # Returns the result of an SQL statement that should only include a COUNT(*) in the SELECT part. @@ -1392,7 +1388,7 @@ module ActiveRecord #:nodoc: def reset_column_information undefine_attribute_methods @column_names = @columns = @columns_hash = @content_columns = @dynamic_methods_hash = @inheritance_column = nil - @active_relation = @active_relation_engine = nil + @arel_engine = @unscoped = @arel_table = nil end def reset_column_information_and_inheritable_attributes_for_all_subclasses#:nodoc: @@ -1505,20 +1501,21 @@ module ActiveRecord #:nodoc: "(#{segments.join(') AND (')})" unless segments.empty? end - def active_relation - @active_relation ||= Relation.new(self, active_relation_table) + def unscoped + @unscoped ||= Relation.new(self, arel_table) + finder_needs_type_condition? ? @unscoped.where(type_condition) : @unscoped end - def active_relation_table(table_name_alias = nil) - Arel::Table.new(table_name, :as => table_name_alias, :engine => active_relation_engine) + def arel_table + @arel_table ||= Arel::Table.new(table_name, :engine => arel_engine) end - def active_relation_engine - @active_relation_engine ||= begin + def arel_engine + @arel_engine ||= begin if self == ActiveRecord::Base Arel::Table.engine else - connection_handler.connection_pools[name] ? Arel::Sql::Engine.new(self) : superclass.active_relation_engine + connection_handler.connection_pools[name] ? Arel::Sql::Engine.new(self) : superclass.arel_engine end end end @@ -1565,111 +1562,36 @@ module ActiveRecord #:nodoc: end end - def default_select(qualified) - if qualified - quoted_table_name + '.*' - else - '*' - end - end - - def construct_finder_arel(options = {}, scope = scope(:find)) - validate_find_options(options) - - 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]))). - group(options[:group] || (scope && scope[:group])). - having(options[:having] || (scope && scope[:having])). - order(construct_order(options[:order], scope)). - limit(construct_limit(options[:limit], scope)). - offset(construct_offset(options[:offset], scope)). - from(options[:from]). - includes( merge_includes(scope && scope[:include], options[:include])) - - lock = (scope && scope[:lock]) || options[:lock] - relation = relation.lock if lock.present? - - relation = relation.readonly if options[:readonly] - + def construct_finder_arel(options = {}, scope = nil) + relation = unscoped.apply_finder_options(options) + relation = scope.merge(relation) if scope 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 + def construct_join(joins) + case joins when Symbol, Hash, Array - if array_of_strings?(merged_joins) - merged_joins.join(' ') + " " + if array_of_strings?(joins) + joins.join(' ') + " " else - build_association_joins(merged_joins) + build_association_joins(joins) end when String - " #{merged_joins} " + " #{joins} " else "" end end - def construct_order(order, scope) - orders = [] - - scoped_order = scope[:order] if scope - if order - orders << order - orders << scoped_order if scoped_order && scoped_order != order - elsif scoped_order - orders << scoped_order - end - - orders.reject {|o| o.blank?} - end - - def construct_limit(limit, scope) - limit ||= scope[:limit] if scope - limit - end - - def construct_offset(offset, scope) - offset ||= scope[:offset] if scope - offset - end - - def construct_conditions(conditions, scope) - conditions = [conditions] - conditions << scope[:conditions] if scope - conditions << type_condition if finder_needs_type_condition? - merge_conditions(*conditions) - end - - # Merges includes so that the result is a valid +include+ - def merge_includes(first, second) - (Array.wrap(first) + Array.wrap(second)).uniq - end - - def merge_joins(*joins) - if joins.any?{|j| j.is_a?(String) || array_of_strings?(j) } - joins = joins.collect do |join| - join = [join] if join.is_a?(String) - join = build_association_joins(join) unless array_of_strings?(join) - join - end - joins.flatten.map{|j| j.strip}.uniq - else - joins.collect{|j| Array.wrap(j)}.flatten.uniq - end - end - def build_association_joins(joins) join_dependency = ActiveRecord::Associations::ClassMethods::JoinDependency.new(self, joins, nil) - relation = active_relation.relation + relation = unscoped.table 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), - Arel::InnerJoin.new(relation, association_relation.last, association.association_join.last).joins(relation)].join() + [Arel::InnerJoin.new(relation, association_relation.first, *association.association_join.first).joins(relation), + Arel::InnerJoin.new(relation, association_relation.last, *association.association_join.last).joins(relation)].join() else - Arel::InnerJoin.new(relation, association_relation, association.association_join).joins(relation) + Arel::InnerJoin.new(relation, association_relation, *association.association_join).joins(relation) end }.join(" ") end @@ -1678,14 +1600,12 @@ module ActiveRecord #:nodoc: o.is_a?(Array) && o.all?{|obj| obj.is_a?(String)} 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] + def type_condition + sti_column = arel_table[inheritance_column] condition = sti_column.eq(sti_name) subclasses.each{|subclass| condition = condition.or(sti_column.eq(subclass.sti_name)) } - condition.to_sql + condition end # Guesses the table name, but does not decorate it with prefix and suffix information. @@ -1714,7 +1634,7 @@ module ActiveRecord #:nodoc: super unless all_attributes_exists?(attribute_names) if match.finder? options = arguments.extract_options! - relation = options.any? ? construct_finder_arel(options) : scoped + relation = options.any? ? construct_finder_arel(options, current_scoped_methods) : 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 @@ -1831,52 +1751,43 @@ module ActiveRecord #:nodoc: def with_scope(method_scoping = {}, action = :merge, &block) method_scoping = method_scoping.method_scoping if method_scoping.respond_to?(:method_scoping) - # Dup first and second level of hash (method and params). - method_scoping = method_scoping.inject({}) do |hash, (method, params)| - hash[method] = (params == true) ? params : params.dup - hash - end + if method_scoping.is_a?(Hash) + # Dup first and second level of hash (method and params). + method_scoping = method_scoping.inject({}) do |hash, (method, params)| + hash[method] = (params == true) ? params : params.dup + hash + end + + method_scoping.assert_valid_keys([ :find, :create ]) + relation = construct_finder_arel(method_scoping[:find] || {}) + + if current_scoped_methods && current_scoped_methods.create_with_value && method_scoping[:create] + scope_for_create = case action + when :merge + current_scoped_methods.create_with_value.merge(method_scoping[:create]) + when :reverse_merge + method_scoping[:create].merge(current_scoped_methods.create_with_value) + else + method_scoping[:create] + end - method_scoping.assert_valid_keys([ :find, :create ]) + relation = relation.create_with(scope_for_create) + else + scope_for_create = method_scoping[:create] + scope_for_create ||= current_scoped_methods.create_with_value if current_scoped_methods + relation = relation.create_with(scope_for_create) if scope_for_create + end - if f = method_scoping[:find] - f.assert_valid_keys(VALID_FIND_OPTIONS) - set_readonly_option! f + method_scoping = relation end - # Merge scopings - if [:merge, :reverse_merge].include?(action) && current_scoped_methods - method_scoping = current_scoped_methods.inject(method_scoping) do |hash, (method, params)| - case hash[method] - when Hash - if method == :find - (hash[method].keys + params.keys).uniq.each do |key| - merge = hash[method][key] && params[key] # merge if both scopes have the same key - if key == :conditions && merge - if params[key].is_a?(Hash) && hash[method][key].is_a?(Hash) - hash[method][key] = merge_conditions(hash[method][key].deep_merge(params[key])) - else - hash[method][key] = merge_conditions(params[key], hash[method][key]) - end - elsif key == :include && merge - hash[method][key] = merge_includes(hash[method][key], params[key]).uniq - elsif key == :joins && merge - hash[method][key] = merge_joins(params[key], hash[method][key]) - else - hash[method][key] = hash[method][key] || params[key] - end - end - else - if action == :reverse_merge - hash[method] = hash[method].merge(params) - else - hash[method] = params.merge(hash[method]) - end - end - else - hash[method] = params - end - hash + if current_scoped_methods + case action + when :merge + method_scoping = current_scoped_methods.merge(method_scoping) + when :reverse_merge + method_scoping = current_scoped_methods.except(:where).merge(method_scoping) + method_scoping = method_scoping.merge(current_scoped_methods.only(:where)) end end @@ -1905,21 +1816,7 @@ module ActiveRecord #:nodoc: # default_scope :order => 'last_name, first_name' # end def default_scope(options = {}) - self.default_scoping << { :find => options, :create => options[:conditions].is_a?(Hash) ? options[:conditions] : {} } - end - - # Test whether the given method and optional key are scoped. - def scoped?(method, key = nil) #:nodoc: - if current_scoped_methods && (scope = current_scoped_methods[method]) - !key || !scope[key].nil? - end - end - - # Retrieve the scope for the given method and optional key. - def scope(method, key = nil) #:nodoc: - if current_scoped_methods && (scope = current_scoped_methods[method]) - key ? scope[key] : scope - end + self.default_scoping << construct_finder_arel(options) end def scoped_methods #:nodoc: @@ -2039,8 +1936,8 @@ module ActiveRecord #:nodoc: def sanitize_sql_hash_for_conditions(attrs, default_table_name = self.table_name) attrs = expand_hash_conditions_for_aggregates(attrs) - table = Arel::Table.new(default_table_name, active_relation_engine) - builder = PredicateBuilder.new(active_relation_engine) + table = Arel::Table.new(self.table_name, :engine => arel_engine, :as => default_table_name) + builder = PredicateBuilder.new(arel_engine) builder.build_from_hash(attrs, table).map(&:to_sql).join(' AND ') end alias_method :sanitize_sql_hash, :sanitize_sql_hash_for_conditions @@ -2123,25 +2020,6 @@ module ActiveRecord #:nodoc: end end - VALID_FIND_OPTIONS = [ :conditions, :include, :joins, :limit, :offset, - :order, :select, :readonly, :group, :having, :from, :lock ] - - def validate_find_options(options) #:nodoc: - options.assert_valid_keys(VALID_FIND_OPTIONS) - end - - def set_readonly_option!(options) #:nodoc: - # Inherit :readonly from finder scope if set. Otherwise, - # if :joins is not blank then :readonly defaults to true. - unless options.has_key?(:readonly) - if scoped_readonly = scope(:find, :readonly) - options[:readonly] = scoped_readonly - elsif !options[:joins].blank? && !options[:select] - options[:readonly] = true - end - end - end - def encode_quoted_value(value) #:nodoc: quoted_value = connection.quote(value) quoted_value = "'#{quoted_value[1..-2].gsub(/\'/, "\\\\'")}'" if quoted_value.include?("\\\'") # (for ruby mode) " @@ -2160,7 +2038,12 @@ module ActiveRecord #:nodoc: @new_record = true ensure_proper_type self.attributes = attributes unless attributes.nil? - self.class.send(:scope, :create).each { |att,value| self.send("#{att}=", value) } if self.class.send(:scoped?, :create) + + if scope = self.class.send(:current_scoped_methods) + create_with = scope.scope_for_create + create_with.each { |att,value| self.send("#{att}=", value) } if create_with + end + result = yield self if block_given? _run_initialize_callbacks result @@ -2186,7 +2069,11 @@ module ActiveRecord #:nodoc: @attributes_cache = {} @new_record = true ensure_proper_type - self.class.send(:scope, :create).each { |att, value| self.send("#{att}=", value) } if self.class.send(:scoped?, :create) + + if scope = self.class.send(:current_scoped_methods) + create_with = scope.scope_for_create + create_with.each { |att,value| self.send("#{att}=", value) } if create_with + end end # Returns a String, which Action Pack uses for constructing an URL to this @@ -2306,7 +2193,7 @@ module ActiveRecord #:nodoc: # be made (since they can't be persisted). def destroy unless new_record? - self.class.active_relation.where(self.class.active_relation[self.class.primary_key].eq(id)).delete_all + self.class.unscoped.where(self.class.arel_table[self.class.primary_key].eq(id)).delete_all end @destroyed = true @@ -2593,7 +2480,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.active_relation.where(self.class.active_relation[self.class.primary_key].eq(id)).update(attributes_with_values) + self.class.unscoped.where(self.class.arel_table[self.class.primary_key].eq(id)).update(attributes_with_values) end # Creates a record with values matching those of the instance attributes @@ -2606,9 +2493,9 @@ module ActiveRecord #:nodoc: attributes_values = arel_attributes_values new_id = if attributes_values.empty? - self.class.active_relation.insert connection.empty_insert_statement_value + self.class.unscoped.insert connection.empty_insert_statement_value else - self.class.active_relation.insert attributes_values + self.class.unscoped.insert attributes_values end self.id ||= new_id @@ -2703,7 +2590,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.active_relation[name]] = value + attrs[self.class.arel_table[name]] = value end end end diff --git a/activerecord/lib/active_record/calculations.rb b/activerecord/lib/active_record/calculations.rb index 20d287faeb..e4b3caab4e 100644 --- a/activerecord/lib/active_record/calculations.rb +++ b/activerecord/lib/active_record/calculations.rb @@ -46,19 +46,19 @@ module ActiveRecord def count(*args) case args.size when 0 - construct_calculation_arel.count + construct_calculation_arel({}, current_scoped_methods).count when 1 if args[0].is_a?(Hash) options = args[0] distinct = options.has_key?(:distinct) ? options.delete(:distinct) : false - construct_calculation_arel(options).count(options[:select], :distinct => distinct) + construct_calculation_arel(options, current_scoped_methods).count(options[:select], :distinct => distinct) else - construct_calculation_arel.count(args[0]) + construct_calculation_arel({}, current_scoped_methods).count(args[0]) end when 2 column_name, options = args distinct = options.has_key?(:distinct) ? options.delete(:distinct) : false - construct_calculation_arel(options).count(column_name, :distinct => distinct) + construct_calculation_arel(options, current_scoped_methods).count(column_name, :distinct => distinct) else raise ArgumentError, "Unexpected parameters passed to count(): #{args.inspect}" end @@ -141,7 +141,7 @@ module ActiveRecord # Person.minimum(:age, :having => 'min(age) > 17', :group => :last_name) # Selects the minimum age for any family without any minors # Person.sum("2 * age") def calculate(operation, column_name, options = {}) - construct_calculation_arel(options).calculate(operation, column_name, options.slice(:distinct)) + construct_calculation_arel(options, current_scoped_methods).calculate(operation, column_name, options.slice(:distinct)) rescue ThrowResult 0 end @@ -151,49 +151,58 @@ module ActiveRecord options.assert_valid_keys(CALCULATIONS_OPTIONS) end - def construct_calculation_arel(options = {}) + def construct_calculation_arel(options = {}, merge_with_relation = nil) validate_calculation_options(options) options = options.except(:distinct) - scope = scope(:find) - includes = merge_includes(scope ? scope[:include] : [], options[:include]) + merge_with_includes = merge_with_relation ? merge_with_relation.includes_values : [] + includes = (merge_with_includes + Array.wrap(options[:include])).uniq if includes.any? - join_dependency = ActiveRecord::Associations::ClassMethods::JoinDependency.new(self, includes, construct_join(options[:joins], scope)) - construct_calculation_arel_with_included_associations(options, join_dependency) + merge_with_joins = merge_with_relation ? merge_with_relation.joins_values : [] + joins = (merge_with_joins + Array.wrap(options[:joins])).uniq + join_dependency = ActiveRecord::Associations::ClassMethods::JoinDependency.new(self, includes, construct_join(joins)) + construct_calculation_arel_with_included_associations(options, join_dependency, merge_with_relation) else - active_relation. - joins(construct_join(options[:joins], scope)). - from((scope && scope[:from]) || options[:from]). - where(construct_conditions(options[:conditions], scope)). - order(options[:order]). - limit(options[:limit]). - offset(options[:offset]). - group(options[:group]). - having(options[:having]). - select(options[:select] || (scope && scope[:select]) || default_select(options[:joins] || (scope && scope[:joins]))) + relation = unscoped.apply_finder_options(options.slice(:joins, :conditions, :order, :limit, :offset, :group, :having)) + + if merge_with_relation + relation = merge_with_relation.except(:select, :order, :limit, :offset, :group, :from).merge(relation) + end + + from = merge_with_relation.from_value if merge_with_relation && merge_with_relation.from_value.present? + from = options[:from] if from.blank? && options[:from].present? + relation = relation.from(from) + + select = options[:select].presence || (merge_with_relation ? merge_with_relation.select_values.join(", ") : nil) + relation = relation.select(select) + + relation end end - def construct_calculation_arel_with_included_associations(options, join_dependency) - scope = scope(:find) - - relation = active_relation + def construct_calculation_arel_with_included_associations(options, join_dependency, merge_with_relation = nil) + relation = unscoped for association in join_dependency.join_associations relation = association.join_relation(relation) end - relation = relation.joins(construct_join(options[:joins], scope)). - select(column_aliases(join_dependency)). - group(options[:group]). - having(options[:having]). - order(options[:order]). - where(construct_conditions(options[:conditions], scope)). - from((scope && scope[:from]) || options[:from]) + if merge_with_relation + relation.joins_values = (merge_with_relation.joins_values + relation.joins_values).uniq + relation.where_values = merge_with_relation.where_values + + merge_limit = merge_with_relation.taken + end + + relation = relation.apply_finder_options(options.slice(:joins, :group, :having, :order, :conditions, :from)). + select(column_aliases(join_dependency)) + + if !using_limitable_reflections?(join_dependency.reflections) && (merge_limit || options[:limit]) + relation = relation.where(construct_arel_limited_ids_condition(options, join_dependency)) + end - relation = relation.where(construct_arel_limited_ids_condition(options, join_dependency)) if !using_limitable_reflections?(join_dependency.reflections) && ((scope && scope[:limit]) || options[:limit]) - relation = relation.limit(construct_limit(options[:limit], scope)) if using_limitable_reflections?(join_dependency.reflections) + relation = relation.limit(options[:limit] || merge_limit) if using_limitable_reflections?(join_dependency.reflections) relation end diff --git a/activerecord/lib/active_record/callbacks.rb b/activerecord/lib/active_record/callbacks.rb index e2a8f03c8f..aecde5848c 100644 --- a/activerecord/lib/active_record/callbacks.rb +++ b/activerecord/lib/active_record/callbacks.rb @@ -9,7 +9,6 @@ module ActiveRecord # * (-) <tt>valid</tt> # * (1) <tt>before_validation</tt> # * (-) <tt>validate</tt> - # * (-) <tt>validate_on_create</tt> # * (2) <tt>after_validation</tt> # * (3) <tt>before_save</tt> # * (4) <tt>before_create</tt> @@ -223,9 +222,10 @@ module ActiveRecord extend ActiveModel::Callbacks + define_callbacks :validation, :terminator => "result == false", :scope => [:kind, :name] + define_model_callbacks :initialize, :find, :only => :after define_model_callbacks :save, :create, :update, :destroy - define_model_callbacks :validation, :only => [:before, :after] end module ClassMethods @@ -236,6 +236,24 @@ module ActiveRecord send(meth.to_sym, meth.to_sym) end end + + def before_validation(*args, &block) + options = args.last + if options.is_a?(Hash) && options[:on] + options[:if] = Array(options[:if]) + options[:if] << "@_on_validate == :#{options[:on]}" + end + set_callback(:validation, :before, *args, &block) + end + + def after_validation(*args, &block) + options = args.extract_options! + options[:prepend] = true + options[:if] = Array(options[:if]) + options[:if] << "!halted && value != false" + options[:if] << "@_on_validate == :#{options[:on]}" if options[:on] + set_callback(:validation, :after, *(args << options), &block) + end end def create_or_update_with_callbacks #:nodoc: diff --git a/activerecord/lib/active_record/connection_adapters/abstract/connection_pool.rb b/activerecord/lib/active_record/connection_adapters/abstract/connection_pool.rb index 377f2a44c5..bf8c546d2e 100644 --- a/activerecord/lib/active_record/connection_adapters/abstract/connection_pool.rb +++ b/activerecord/lib/active_record/connection_adapters/abstract/connection_pool.rb @@ -56,7 +56,7 @@ module ActiveRecord # * +wait_timeout+: number of seconds to block and wait for a connection # before giving up and raising a timeout error (default 5 seconds). class ConnectionPool - attr_reader :spec + attr_reader :spec, :connections # Creates a new ConnectionPool object. +spec+ is a ConnectionSpecification # object which describes database connection information (e.g. adapter, diff --git a/activerecord/lib/active_record/connection_adapters/abstract/connection_specification.rb b/activerecord/lib/active_record/connection_adapters/abstract/connection_specification.rb index bbc290f721..2f36bec764 100644 --- a/activerecord/lib/active_record/connection_adapters/abstract/connection_specification.rb +++ b/activerecord/lib/active_record/connection_adapters/abstract/connection_specification.rb @@ -51,8 +51,8 @@ module ActiveRecord def self.establish_connection(spec = nil) case spec when nil - raise AdapterNotSpecified unless defined? RAILS_ENV - establish_connection(RAILS_ENV) + raise AdapterNotSpecified unless defined?(Rails.env) + establish_connection(Rails.env) when ConnectionSpecification @@connection_handler.establish_connection(name, spec) when Symbol, String @@ -88,26 +88,6 @@ module ActiveRecord end class << self - # Deprecated and no longer has any effect. - def allow_concurrency - ActiveSupport::Deprecation.warn("ActiveRecord::Base.allow_concurrency has been deprecated and no longer has any effect. Please remove all references to allow_concurrency.") - end - - # Deprecated and no longer has any effect. - def allow_concurrency=(flag) - ActiveSupport::Deprecation.warn("ActiveRecord::Base.allow_concurrency= has been deprecated and no longer has any effect. Please remove all references to allow_concurrency=.") - end - - # Deprecated and no longer has any effect. - def verification_timeout - ActiveSupport::Deprecation.warn("ActiveRecord::Base.verification_timeout has been deprecated and no longer has any effect. Please remove all references to verification_timeout.") - end - - # Deprecated and no longer has any effect. - def verification_timeout=(flag) - ActiveSupport::Deprecation.warn("ActiveRecord::Base.verification_timeout= has been deprecated and no longer has any effect. Please remove all references to verification_timeout=.") - end - # Returns the connection currently associated with the class. This can # also be used to "borrow" the connection to do database work unrelated # to any of the specific Active Records. diff --git a/activerecord/lib/active_record/connection_adapters/abstract/query_cache.rb b/activerecord/lib/active_record/connection_adapters/abstract/query_cache.rb index 00c71090f3..020acbbe5a 100644 --- a/activerecord/lib/active_record/connection_adapters/abstract/query_cache.rb +++ b/activerecord/lib/active_record/connection_adapters/abstract/query_cache.rb @@ -75,7 +75,8 @@ module ActiveRecord def cache_sql(sql) result = if @query_cache.has_key?(sql) - log_info(sql, "CACHE", 0.0) + ActiveSupport::Notifications.instrument("active_record.sql", + :sql => sql, :name => "CACHE", :connection_id => self.object_id) @query_cache[sql] else @query_cache[sql] = yield diff --git a/activerecord/lib/active_record/connection_adapters/abstract/schema_statements.rb b/activerecord/lib/active_record/connection_adapters/abstract/schema_statements.rb index e731bc84f0..86ba7d72c3 100644 --- a/activerecord/lib/active_record/connection_adapters/abstract/schema_statements.rb +++ b/activerecord/lib/active_record/connection_adapters/abstract/schema_statements.rb @@ -344,12 +344,12 @@ module ActiveRecord end end - def assume_migrated_upto_version(version) + def assume_migrated_upto_version(version, migrations_path = ActiveRecord::Migrator.migrations_path) version = version.to_i sm_table = quote_table_name(ActiveRecord::Migrator.schema_migrations_table_name) migrated = select_values("SELECT version FROM #{sm_table}").map(&:to_i) - versions = Dir['db/migrate/[0-9]*_*.rb'].map do |filename| + versions = Dir["#{migrations_path}/[0-9]*_*.rb"].map do |filename| filename.split('/').last.split('_').first.to_i end diff --git a/activerecord/lib/active_record/connection_adapters/abstract_adapter.rb b/activerecord/lib/active_record/connection_adapters/abstract_adapter.rb index d09aa3c4d2..7e80347f75 100755 --- a/activerecord/lib/active_record/connection_adapters/abstract_adapter.rb +++ b/activerecord/lib/active_record/connection_adapters/abstract_adapter.rb @@ -39,7 +39,6 @@ module ActiveRecord def initialize(connection, logger = nil) #:nodoc: @connection, @logger = connection, logger @runtime = 0 - @last_verification = 0 @query_cache_enabled = false end @@ -191,27 +190,19 @@ module ActiveRecord "active_record_#{open_transactions}" end - def log_info(sql, name, ms) - if @logger && @logger.debug? - name = '%s (%.1fms)' % [name || 'SQL', ms] - @logger.debug(format_log_entry(name, sql.squeeze(' '))) - end - end - protected + def log(sql, name) + name ||= "SQL" result = nil - ActiveSupport::Notifications.instrument(:sql, :sql => sql, :name => name) do + ActiveSupport::Notifications.instrument("active_record.sql", + :sql => sql, :name => name, :connection_id => self.object_id) do @runtime += Benchmark.ms { result = yield } end result rescue Exception => e - # Log message and raise exception. - # Set last_verification to 0, so that connection gets verified - # upon reentering the request loop - @last_verification = 0 message = "#{e.class.name}: #{e.message}: #{sql}" - log_info(message, name, 0) + @logger.debug message if @logger raise translate_exception(e, message) end @@ -220,23 +211,6 @@ module ActiveRecord ActiveRecord::StatementInvalid.new(message) end - def format_log_entry(message, dump = nil) - if ActiveRecord::Base.colorize_logging - if @@row_even - @@row_even = false - message_color, dump_color = "4;36;1", "0;1" - else - @@row_even = true - message_color, dump_color = "4;35;1", "0" - end - - log_entry = " \e[#{message_color}m#{message}\e[0m " - log_entry << "\e[#{dump_color}m%#{String === dump ? 's' : 'p'}\e[0m" % dump if dump - log_entry - else - "%s %s" % [message, dump] - end - end end end end diff --git a/activerecord/lib/active_record/connection_adapters/mysql_adapter.rb b/activerecord/lib/active_record/connection_adapters/mysql_adapter.rb index fa28bc64df..8c0bf6396a 100644 --- a/activerecord/lib/active_record/connection_adapters/mysql_adapter.rb +++ b/activerecord/lib/active_record/connection_adapters/mysql_adapter.rb @@ -579,6 +579,8 @@ module ActiveRecord protected def translate_exception(exception, message) + return super unless exception.respond_to?(:errno) + case exception.errno when 1062 RecordNotUnique.new(message, exception) diff --git a/activerecord/lib/active_record/connection_adapters/sqlite_adapter.rb b/activerecord/lib/active_record/connection_adapters/sqlite_adapter.rb index 78b897add6..0a52f3a6a2 100644 --- a/activerecord/lib/active_record/connection_adapters/sqlite_adapter.rb +++ b/activerecord/lib/active_record/connection_adapters/sqlite_adapter.rb @@ -14,7 +14,7 @@ module ActiveRecord # Allow database path relative to Rails.root, but only if # the database path is not the special path that tells # Sqlite to build a database only in memory. - if Object.const_defined?(:Rails) && ':memory:' != config[:database] + if defined?(Rails.root) && ':memory:' != config[:database] config[:database] = File.expand_path(config[:database], Rails.root) end end diff --git a/activerecord/lib/active_record/fixtures.rb b/activerecord/lib/active_record/fixtures.rb index c94b31a856..79f70f07cd 100644 --- a/activerecord/lib/active_record/fixtures.rb +++ b/activerecord/lib/active_record/fixtures.rb @@ -3,7 +3,6 @@ require 'yaml' require 'csv' require 'zlib' require 'active_support/dependencies' -require 'active_support/test_case' require 'active_support/core_ext/logger' if RUBY_VERSION < '1.9' @@ -434,7 +433,7 @@ end # Any fixture labeled "DEFAULTS" is safely ignored. class Fixtures < (RUBY_VERSION < '1.9' ? YAML::Omap : Hash) - MAX_ID = 2 ** 31 - 1 + MAX_ID = 2 ** 30 - 1 DEFAULT_FILTER_RE = /\.ya?ml$/ @@all_cached_fixtures = {} @@ -822,8 +821,8 @@ module ActiveRecord superclass_delegating_accessor :pre_loaded_fixtures self.fixture_table_names = [] - self.use_transactional_fixtures = false - self.use_instantiated_fixtures = true + self.use_transactional_fixtures = true + self.use_instantiated_fixtures = false self.pre_loaded_fixtures = false self.fixture_class_names = {} diff --git a/activerecord/lib/active_record/locale/en.yml b/activerecord/lib/active_record/locale/en.yml index e33d389f8c..4115cc8e17 100644 --- a/activerecord/lib/active_record/locale/en.yml +++ b/activerecord/lib/active_record/locale/en.yml @@ -1,33 +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: - inclusion: "is not included in the list" - exclusion: "is reserved" - invalid: "is invalid" - confirmation: "doesn't match confirmation" - accepted: "must be accepted" - empty: "can't be empty" - blank: "can't be blank" - too_long: "is too long (maximum is {{count}} characters)" - too_short: "is too short (minimum is {{count}} characters)" - wrong_length: "is the wrong length (should be {{count}} characters)" - taken: "has already been taken" - not_a_number: "is not a number" - greater_than: "must be greater than {{count}}" - greater_than_or_equal_to: "must be greater than or equal to {{count}}" - equal_to: "must be equal to {{count}}" - less_than: "must be less than {{count}}" - less_than_or_equal_to: "must be less than or equal to {{count}}" - odd: "must be odd" - even: "must be even" - record_invalid: "Validation failed: {{errors}}" - # Append your own errors here or at the model/attributes scope. + errors: + messages: + taken: "has already been taken" + record_invalid: "Validation failed: {{errors}}" + # Append your own errors here or at the model/attributes scope. # You can define own errors for models or model attributes. # The values :model, :attribute and :value are always available for interpolation. @@ -42,7 +18,14 @@ en: # Will define custom blank validation message for User model and # custom blank validation message for login attribute of User model. #models: - + + # Attributes names common to most models + #attributes: + #created_at: "Created at" + #updated_at: "Updated at" + + # ActiveRecord models configuration + #activerecord: # Translate model names. Used in Model.human_name(). #models: # For example, @@ -55,4 +38,3 @@ en: # user: # login: "Handle" # will translate User attribute "login" as "Handle" - diff --git a/activerecord/lib/active_record/locking/optimistic.rb b/activerecord/lib/active_record/locking/optimistic.rb index f9e538c586..9fcdabdb44 100644 --- a/activerecord/lib/active_record/locking/optimistic.rb +++ b/activerecord/lib/active_record/locking/optimistic.rb @@ -78,7 +78,7 @@ module ActiveRecord attribute_names.uniq! begin - relation = self.class.active_relation + relation = self.class.unscoped affected_rows = relation.where( relation[self.class.primary_key].eq(quoted_id).and( diff --git a/activerecord/lib/active_record/migration.rb b/activerecord/lib/active_record/migration.rb index c218c5bfd1..c059b2d18b 100644 --- a/activerecord/lib/active_record/migration.rb +++ b/activerecord/lib/active_record/migration.rb @@ -340,6 +340,10 @@ module ActiveRecord self.verbose = save end + def connection + ActiveRecord::Base.connection + end + def method_missing(method, *arguments, &block) arg_list = arguments.map(&:inspect) * ', ' @@ -347,7 +351,7 @@ module ActiveRecord unless arguments.empty? || method == :execute arguments[0] = Migrator.proper_table_name(arguments.first) end - Base.connection.send(method, *arguments, &block) + connection.send(method, *arguments, &block) end end end @@ -404,6 +408,10 @@ module ActiveRecord self.new(direction, migrations_path, target_version).run end + def migrations_path + 'db/migrate' + end + def schema_migrations_table_name Base.table_name_prefix + 'schema_migrations' + Base.table_name_suffix end diff --git a/activerecord/lib/active_record/named_scope.rb b/activerecord/lib/active_record/named_scope.rb index f63b249241..90fd700437 100644 --- a/activerecord/lib/active_record/named_scope.rb +++ b/activerecord/lib/active_record/named_scope.rb @@ -26,10 +26,12 @@ module ActiveRecord if options.present? Scope.new(self, options, &block) else - unless scoped?(:find) - finder_needs_type_condition? ? active_relation.where(type_condition) : active_relation.spawn + current_scope = current_scoped_methods + + unless current_scope + unscoped.spawn else - construct_finder_arel + construct_finder_arel({}, current_scoped_methods) end end end diff --git a/activerecord/lib/active_record/nested_attributes.rb b/activerecord/lib/active_record/nested_attributes.rb index dbdeba6c24..d8fae495e7 100644 --- a/activerecord/lib/active_record/nested_attributes.rb +++ b/activerecord/lib/active_record/nested_attributes.rb @@ -188,6 +188,8 @@ module ActiveRecord # the parent model is saved. This happens inside the transaction initiated # by the parents save method. See ActiveRecord::AutosaveAssociation. module ClassMethods + REJECT_ALL_BLANK_PROC = proc { |attributes| attributes.all? { |_, value| value.blank? } } + # Defines an attributes writer for the specified association(s). If you # are using <tt>attr_protected</tt> or <tt>attr_accessible</tt>, then you # will need to add the attribute writer to the allowed list. @@ -229,23 +231,14 @@ module ActiveRecord options = { :allow_destroy => false, :update_only => false } options.update(attr_names.extract_options!) options.assert_valid_keys(:allow_destroy, :reject_if, :limit, :update_only) + options[:reject_if] = REJECT_ALL_BLANK_PROC if options[:reject_if] == :all_blank attr_names.each do |association_name| if reflection = reflect_on_association(association_name) - type = case reflection.macro - when :has_one, :belongs_to - :one_to_one - when :has_many, :has_and_belongs_to_many - :collection - end - reflection.options[:autosave] = true add_autosave_association_callbacks(reflection) - self.nested_attributes_options[association_name.to_sym] = options - - if options[:reject_if] == :all_blank - self.nested_attributes_options[association_name.to_sym][:reject_if] = proc { |attributes| attributes.all? {|k,v| v.blank?} } - end + nested_attributes_options[association_name.to_sym] = options + type = (reflection.collection? ? :collection : :one_to_one) # def pirate_attributes=(attributes) # assign_nested_attributes_for_one_to_one_association(:pirate, attributes) @@ -271,21 +264,11 @@ module ActiveRecord marked_for_destruction? end - # Deal with deprecated _delete. - # - def _delete #:nodoc: - ActiveSupport::Deprecation.warn "_delete is deprecated in nested attributes. Use _destroy instead." - _destroy - end - private # Attribute hash keys that should not be assigned as normal attributes. # These hash keys are nested attributes implementation details. - # - # TODO Remove _delete from UNASSIGNABLE_KEYS when deprecation warning are - # removed. - UNASSIGNABLE_KEYS = %w( id _destroy _delete ) + UNASSIGNABLE_KEYS = %w( id _destroy ) # Assigns the given attributes to the association. # @@ -298,13 +281,17 @@ module ActiveRecord # update_only is true, and a <tt>:_destroy</tt> key set to a truthy value, # then the existing record will be marked for destruction. def assign_nested_attributes_for_one_to_one_association(association_name, attributes) - options = self.nested_attributes_options[association_name] + options = nested_attributes_options[association_name] attributes = attributes.with_indifferent_access check_existing_record = (options[:update_only] || !attributes['id'].blank?) if check_existing_record && (record = send(association_name)) && (options[:update_only] || record.id.to_s == attributes['id'].to_s) assign_to_or_mark_for_destruction(record, attributes, options[:allow_destroy]) + + elsif attributes['id'] + raise_nested_attributes_record_not_found(association_name, attributes['id']) + elsif !reject_new_record?(association_name, attributes) method = "build_#{association_name}" if respond_to?(method) @@ -343,7 +330,7 @@ module ActiveRecord # { :id => '2', :_destroy => true } # ]) def assign_nested_attributes_for_collection_association(association_name, attributes_collection) - options = self.nested_attributes_options[association_name] + options = nested_attributes_options[association_name] unless attributes_collection.is_a?(Hash) || attributes_collection.is_a?(Array) raise ArgumentError, "Hash or Array expected, got #{attributes_collection.class.name} (#{attributes_collection.inspect})" @@ -366,6 +353,8 @@ module ActiveRecord end elsif existing_record = send(association_name).detect { |record| record.id.to_s == attributes['id'].to_s } assign_to_or_mark_for_destruction(existing_record, attributes, options[:allow_destroy]) + else + raise_nested_attributes_record_not_found(association_name, attributes['id']) end end end @@ -382,8 +371,7 @@ module ActiveRecord # Determines if a hash contains a truthy _destroy key. def has_destroy_flag?(hash) - ConnectionAdapters::Column.value_to_boolean(hash['_destroy']) || - ConnectionAdapters::Column.value_to_boolean(hash['_delete']) # TODO Remove after deprecation. + ConnectionAdapters::Column.value_to_boolean(hash['_destroy']) end # Determines if a new record should be build by checking for @@ -394,14 +382,17 @@ module ActiveRecord end def call_reject_if(association_name, attributes) - callback = self.nested_attributes_options[association_name][:reject_if] - - case callback + case callback = nested_attributes_options[association_name][:reject_if] when Symbol method(callback).arity == 0 ? send(callback) : send(callback, attributes) when Proc - callback.try(:call, attributes) + callback.call(attributes) end end + + def raise_nested_attributes_record_not_found(association_name, record_id) + reflection = self.class.reflect_on_association(association_name) + raise RecordNotFound, "Couldn't find #{reflection.klass.name} with ID=#{record_id} for #{self.class.name} with ID=#{id}" + end end end diff --git a/activerecord/lib/active_record/railtie.rb b/activerecord/lib/active_record/railtie.rb index 657ee738c0..bc06333f1c 100644 --- a/activerecord/lib/active_record/railtie.rb +++ b/activerecord/lib/active_record/railtie.rb @@ -14,6 +14,10 @@ module ActiveRecord load "active_record/railties/databases.rake" end + # TODO If we require the wrong file, the error never comes up. + require "active_record/railties/subscriber" + subscriber ActiveRecord::Railties::Subscriber.new + initializer "active_record.set_configs" do |app| app.config.active_record.each do |k,v| ActiveRecord::Base.send "#{k}=", v @@ -52,6 +56,19 @@ module ActiveRecord initializer "active_record.load_observers" do ActiveRecord::Base.instantiate_observers + + ActionDispatch::Callbacks.to_prepare(:activerecord_instantiate_observers) do + ActiveRecord::Base.instantiate_observers + end + end + + initializer "active_record.set_dispatch_hooks", :before => :set_clear_dependencies_hook do |app| + unless app.config.cache_classes + ActionDispatch::Callbacks.after do + ActiveRecord::Base.reset_subclasses + ActiveRecord::Base.clear_reloadable_connections! + end + end end # TODO: ActiveRecord::Base.logger should delegate to its own config.logger @@ -59,13 +76,16 @@ module ActiveRecord ActiveRecord::Base.logger ||= ::Rails.logger end - initializer "active_record.notifications" do - require 'active_support/notifications' + initializer "active_record.i18n_deprecation" do + require 'active_support/i18n' - ActiveSupport::Notifications.subscribe("sql") do |name, before, after, instrumenter_id, payload| - ActiveRecord::Base.connection.log_info(payload[:sql], payload[:name], (after - before) * 1000) + begin + I18n.t(:"activerecord.errors", :raise => true) + warn "[DEPRECATION] \"activerecord.errors\" namespace is deprecated in I18n " << + "yml files, please use just \"errors\" instead." + rescue Exception => e + # No message then. end end - end end diff --git a/activerecord/lib/active_record/railties/controller_runtime.rb b/activerecord/lib/active_record/railties/controller_runtime.rb index 535e967ec3..aed1c59b00 100644 --- a/activerecord/lib/active_record/railties/controller_runtime.rb +++ b/activerecord/lib/active_record/railties/controller_runtime.rb @@ -5,6 +5,8 @@ module ActiveRecord module ControllerRuntime extend ActiveSupport::Concern + protected + attr_internal :db_runtime def cleanup_view_runtime @@ -19,11 +21,16 @@ module ActiveRecord end end + def append_info_to_payload(payload) + super + payload[:db_runtime] = db_runtime + end + module ClassMethods - def log_process_action(controller) - super - db_runtime = controller.send :db_runtime - logger.info(" ActiveRecord runtime: %.1fms" % db_runtime.to_f) if db_runtime + def log_process_action(payload) + messages, db_runtime = super, payload[:db_runtime] + messages << ("ActiveRecord: %.1fms" % db_runtime.to_f) if db_runtime + messages end end end diff --git a/activerecord/lib/active_record/railties/databases.rake b/activerecord/lib/active_record/railties/databases.rake index a35a6c156b..b39e064e45 100644 --- a/activerecord/lib/active_record/railties/databases.rake +++ b/activerecord/lib/active_record/railties/databases.rake @@ -26,9 +26,9 @@ namespace :db do end end - desc 'Create the database defined in config/database.yml for the current RAILS_ENV' + desc 'Create the database defined in config/database.yml for the current Rails.env' task :create => :load_config do - create_database(ActiveRecord::Base.configurations[RAILS_ENV]) + create_database(ActiveRecord::Base.configurations[Rails.env]) end def create_database(config) @@ -46,7 +46,7 @@ namespace :db do $stderr.puts "Couldn't create database for #{config.inspect}" end end - return # Skip the else clause of begin/rescue + return # Skip the else clause of begin/rescue else ActiveRecord::Base.establish_connection(config) ActiveRecord::Base.connection @@ -111,9 +111,9 @@ namespace :db do end end - desc 'Drops the database for the current RAILS_ENV' + desc 'Drops the database for the current Rails.env' task :drop => :load_config do - config = ActiveRecord::Base.configurations[RAILS_ENV || 'development'] + config = ActiveRecord::Base.configurations[Rails.env || 'development'] begin drop_database(config) rescue Exception => e @@ -188,7 +188,7 @@ namespace :db do desc "Retrieves the charset for the current environment's database" task :charset => :environment do - config = ActiveRecord::Base.configurations[RAILS_ENV || 'development'] + config = ActiveRecord::Base.configurations[Rails.env || 'development'] case config['adapter'] when 'mysql' ActiveRecord::Base.establish_connection(config) @@ -203,7 +203,7 @@ namespace :db do desc "Retrieves the collation for the current environment's database" task :collation => :environment do - config = ActiveRecord::Base.configurations[RAILS_ENV || 'development'] + config = ActiveRecord::Base.configurations[Rails.env || 'development'] case config['adapter'] when 'mysql' ActiveRecord::Base.establish_connection(config) @@ -246,6 +246,7 @@ namespace :db do desc "Load fixtures into the current environment's database. Load specific fixtures using FIXTURES=x,y. Load from subdirectory in test/fixtures using FIXTURES_DIR=z. Specify an alternative path (eg. spec/fixtures) using FIXTURES_PATH=spec/fixtures." task :load => :environment do require 'active_record/fixtures' + ActiveRecord::Base.establish_connection(Rails.env) base_dir = ENV['FIXTURES_PATH'] ? File.join(Rails.root, ENV['FIXTURES_PATH']) : File.join(Rails.root, 'test', 'fixtures') fixtures_dir = ENV['FIXTURES_DIR'] ? File.join(base_dir, ENV['FIXTURES_DIR']) : base_dir @@ -257,7 +258,7 @@ namespace :db do desc "Search for a fixture given a LABEL or ID. Specify an alternative path (eg. spec/fixtures) using FIXTURES_PATH=spec/fixtures." task :identify => :environment do - require "active_record/fixtures" + require 'active_record/fixtures' label, id = ENV["LABEL"], ENV["ID"] raise "LABEL or ID required" if label.blank? && id.blank? @@ -295,7 +296,7 @@ namespace :db do if File.exists?(file) load(file) else - abort %{#{file} doesn't exist yet. Run "rake db:migrate" to create it then try again. If you do not intend to use a database, you should instead alter #{Rails.root}/config/application.rb to prevent active_record from loading: config.frameworks -= [ :active_record ]} + abort %{#{file} doesn't exist yet. Run "rake db:migrate" to create it then try again. If you do not intend to use a database, you should instead alter #{Rails.root}/config/boot.rb to limit the frameworks that will be loaded} end end end @@ -304,36 +305,36 @@ namespace :db do desc "Dump the database structure to a SQL file" task :dump => :environment do abcs = ActiveRecord::Base.configurations - case abcs[RAILS_ENV]["adapter"] + case abcs[Rails.env]["adapter"] when "mysql", "oci", "oracle" - ActiveRecord::Base.establish_connection(abcs[RAILS_ENV]) - File.open("#{Rails.root}/db/#{RAILS_ENV}_structure.sql", "w+") { |f| f << ActiveRecord::Base.connection.structure_dump } + ActiveRecord::Base.establish_connection(abcs[Rails.env]) + File.open("#{Rails.root}/db/#{Rails.env}_structure.sql", "w+") { |f| f << ActiveRecord::Base.connection.structure_dump } when "postgresql" - ENV['PGHOST'] = abcs[RAILS_ENV]["host"] if abcs[RAILS_ENV]["host"] - ENV['PGPORT'] = abcs[RAILS_ENV]["port"].to_s if abcs[RAILS_ENV]["port"] - ENV['PGPASSWORD'] = abcs[RAILS_ENV]["password"].to_s if abcs[RAILS_ENV]["password"] - search_path = abcs[RAILS_ENV]["schema_search_path"] + ENV['PGHOST'] = abcs[Rails.env]["host"] if abcs[Rails.env]["host"] + ENV['PGPORT'] = abcs[Rails.env]["port"].to_s if abcs[Rails.env]["port"] + ENV['PGPASSWORD'] = abcs[Rails.env]["password"].to_s if abcs[Rails.env]["password"] + search_path = abcs[Rails.env]["schema_search_path"] unless search_path.blank? search_path = search_path.split(",").map{|search_path| "--schema=#{search_path.strip}" }.join(" ") end - `pg_dump -i -U "#{abcs[RAILS_ENV]["username"]}" -s -x -O -f db/#{RAILS_ENV}_structure.sql #{search_path} #{abcs[RAILS_ENV]["database"]}` + `pg_dump -i -U "#{abcs[Rails.env]["username"]}" -s -x -O -f db/#{Rails.env}_structure.sql #{search_path} #{abcs[Rails.env]["database"]}` raise "Error dumping database" if $?.exitstatus == 1 when "sqlite", "sqlite3" - dbfile = abcs[RAILS_ENV]["database"] || abcs[RAILS_ENV]["dbfile"] - `#{abcs[RAILS_ENV]["adapter"]} #{dbfile} .schema > db/#{RAILS_ENV}_structure.sql` + dbfile = abcs[Rails.env]["database"] || abcs[Rails.env]["dbfile"] + `#{abcs[Rails.env]["adapter"]} #{dbfile} .schema > db/#{Rails.env}_structure.sql` when "sqlserver" - `scptxfr /s #{abcs[RAILS_ENV]["host"]} /d #{abcs[RAILS_ENV]["database"]} /I /f db\\#{RAILS_ENV}_structure.sql /q /A /r` - `scptxfr /s #{abcs[RAILS_ENV]["host"]} /d #{abcs[RAILS_ENV]["database"]} /I /F db\ /q /A /r` + `scptxfr /s #{abcs[Rails.env]["host"]} /d #{abcs[Rails.env]["database"]} /I /f db\\#{Rails.env}_structure.sql /q /A /r` + `scptxfr /s #{abcs[Rails.env]["host"]} /d #{abcs[Rails.env]["database"]} /I /F db\ /q /A /r` when "firebird" - set_firebird_env(abcs[RAILS_ENV]) - db_string = firebird_db_string(abcs[RAILS_ENV]) - sh "isql -a #{db_string} > #{Rails.root}/db/#{RAILS_ENV}_structure.sql" + set_firebird_env(abcs[Rails.env]) + db_string = firebird_db_string(abcs[Rails.env]) + sh "isql -a #{db_string} > #{Rails.root}/db/#{Rails.env}_structure.sql" else raise "Task not supported by '#{abcs["test"]["adapter"]}'" end if ActiveRecord::Base.connection.supports_migrations? - File.open("#{Rails.root}/db/#{RAILS_ENV}_structure.sql", "a") { |f| f << ActiveRecord::Base.connection.dump_schema_information } + File.open("#{Rails.root}/db/#{Rails.env}_structure.sql", "a") { |f| f << ActiveRecord::Base.connection.dump_schema_information } end end end @@ -356,28 +357,28 @@ namespace :db do when "mysql" ActiveRecord::Base.establish_connection(:test) ActiveRecord::Base.connection.execute('SET foreign_key_checks = 0') - IO.readlines("#{Rails.root}/db/#{RAILS_ENV}_structure.sql").join.split("\n\n").each do |table| + IO.readlines("#{Rails.root}/db/#{Rails.env}_structure.sql").join.split("\n\n").each do |table| ActiveRecord::Base.connection.execute(table) end when "postgresql" ENV['PGHOST'] = abcs["test"]["host"] if abcs["test"]["host"] ENV['PGPORT'] = abcs["test"]["port"].to_s if abcs["test"]["port"] ENV['PGPASSWORD'] = abcs["test"]["password"].to_s if abcs["test"]["password"] - `psql -U "#{abcs["test"]["username"]}" -f #{Rails.root}/db/#{RAILS_ENV}_structure.sql #{abcs["test"]["database"]}` + `psql -U "#{abcs["test"]["username"]}" -f #{Rails.root}/db/#{Rails.env}_structure.sql #{abcs["test"]["database"]}` when "sqlite", "sqlite3" dbfile = abcs["test"]["database"] || abcs["test"]["dbfile"] - `#{abcs["test"]["adapter"]} #{dbfile} < #{Rails.root}/db/#{RAILS_ENV}_structure.sql` + `#{abcs["test"]["adapter"]} #{dbfile} < #{Rails.root}/db/#{Rails.env}_structure.sql` when "sqlserver" - `osql -E -S #{abcs["test"]["host"]} -d #{abcs["test"]["database"]} -i db\\#{RAILS_ENV}_structure.sql` + `osql -E -S #{abcs["test"]["host"]} -d #{abcs["test"]["database"]} -i db\\#{Rails.env}_structure.sql` when "oci", "oracle" ActiveRecord::Base.establish_connection(:test) - IO.readlines("#{Rails.root}/db/#{RAILS_ENV}_structure.sql").join.split(";\n\n").each do |ddl| + IO.readlines("#{Rails.root}/db/#{Rails.env}_structure.sql").join.split(";\n\n").each do |ddl| ActiveRecord::Base.connection.execute(ddl) end when "firebird" set_firebird_env(abcs["test"]) db_string = firebird_db_string(abcs["test"]) - sh "isql -i #{Rails.root}/db/#{RAILS_ENV}_structure.sql #{db_string}" + sh "isql -i #{Rails.root}/db/#{Rails.env}_structure.sql #{db_string}" else raise "Task not supported by '#{abcs["test"]["adapter"]}'" end @@ -400,7 +401,7 @@ namespace :db do when "sqlserver" dropfkscript = "#{abcs["test"]["host"]}.#{abcs["test"]["database"]}.DP1".gsub(/\\/,'-') `osql -E -S #{abcs["test"]["host"]} -d #{abcs["test"]["database"]} -i db\\#{dropfkscript}` - `osql -E -S #{abcs["test"]["host"]} -d #{abcs["test"]["database"]} -i db\\#{RAILS_ENV}_structure.sql` + `osql -E -S #{abcs["test"]["host"]} -d #{abcs["test"]["database"]} -i db\\#{Rails.env}_structure.sql` when "oci", "oracle" ActiveRecord::Base.establish_connection(:test) ActiveRecord::Base.connection.structure_drop.split(";\n\n").each do |ddl| diff --git a/activerecord/lib/active_record/railties/subscriber.rb b/activerecord/lib/active_record/railties/subscriber.rb new file mode 100644 index 0000000000..7c2a10cf0f --- /dev/null +++ b/activerecord/lib/active_record/railties/subscriber.rb @@ -0,0 +1,27 @@ +module ActiveRecord + module Railties + class Subscriber < Rails::Subscriber + def sql(event) + name = '%s (%.1fms)' % [event.payload[:name], event.duration] + sql = event.payload[:sql].squeeze(' ') + + if odd? + name = color(name, :cyan, true) + sql = color(sql, nil, true) + else + name = color(name, :magenta, true) + end + + debug "#{name} #{sql}" + end + + def odd? + @odd_or_even = !@odd_or_even + end + + def logger + ActiveRecord::Base.logger + end + end + end +end
\ No newline at end of file diff --git a/activerecord/lib/active_record/reflection.rb b/activerecord/lib/active_record/reflection.rb index b751c9ad68..32b9a2aa87 100644 --- a/activerecord/lib/active_record/reflection.rb +++ b/activerecord/lib/active_record/reflection.rb @@ -252,10 +252,33 @@ module ActiveRecord end end + # Returns whether or not this association reflection is for a collection + # association. Returns +true+ if the +macro+ is one of +has_many+ or + # +has_and_belongs_to_many+, +false+ otherwise. + def collection? + if @collection.nil? + @collection = [:has_many, :has_and_belongs_to_many].include?(macro) + end + @collection + end + + # Returns whether or not the association should be validated as part of + # the parent's validation. + # + # Unless you explicitely disable validation with + # <tt>:validate => false</tt>, it will take place when: + # + # * you explicitely enable validation; <tt>:validate => true</tt> + # * you use autosave; <tt>:autosave => true</tt> + # * the association is a +has_many+ association + def validate? + !options[:validate].nil? ? options[:validate] : (options[:autosave] == true || macro == :has_many) + end + private def derive_class_name class_name = name.to_s.camelize - class_name = class_name.singularize if [ :has_many, :has_and_belongs_to_many ].include?(macro) + class_name = class_name.singularize if collection? class_name end diff --git a/activerecord/lib/active_record/relation.rb b/activerecord/lib/active_record/relation.rb index 6b9925d4e7..85bf878416 100644 --- a/activerecord/lib/active_record/relation.rb +++ b/activerecord/lib/active_record/relation.rb @@ -1,19 +1,19 @@ module ActiveRecord class Relation - include QueryMethods, FinderMethods, CalculationMethods, SpawnMethods + JoinOperation = Struct.new(:relation, :join_class, :on) + ASSOCIATION_METHODS = [:includes, :eager_load, :preload] + MULTI_VALUE_METHODS = [:select, :group, :order, :joins, :where, :having] + SINGLE_VALUE_METHODS = [:limit, :offset, :lock, :readonly, :create_with, :from] + + include FinderMethods, CalculationMethods, SpawnMethods, QueryMethods delegate :length, :collect, :map, :each, :all?, :to => :to_a - attr_reader :relation, :klass - attr_writer :readonly, :table - attr_accessor :preload_associations, :eager_load_associations, :includes_associations + attr_reader :table, :klass - def initialize(klass, relation) - @klass, @relation = klass, relation - @preload_associations = [] - @eager_load_associations = [] - @includes_associations = [] - @loaded, @readonly = false + def initialize(klass, table) + @klass, @table = klass, table + (ASSOCIATION_METHODS + MULTI_VALUE_METHODS).each {|v| instance_variable_set(:"@#{v}_values", [])} end def new(*args, &block) @@ -29,7 +29,7 @@ module ActiveRecord end def respond_to?(method, include_private = false) - return true if @relation.respond_to?(method, include_private) || Array.method_defined?(method) + return true if arel.respond_to?(method, include_private) || Array.method_defined?(method) if match = DynamicFinderMatch.match(method) return true if @klass.send(:all_attributes_exists?, match.attribute_names) @@ -43,33 +43,38 @@ module ActiveRecord def to_a return @records if loaded? - find_with_associations = @eager_load_associations.any? || references_eager_loaded_tables? + find_with_associations = @eager_load_values.any? || (@includes_values.any? && references_eager_loaded_tables?) @records = if find_with_associations begin - @klass.send(:find_with_associations, { - :select => @relation.send(:select_clauses).join(', '), - :joins => @relation.joins(relation), - :group => @relation.send(:group_clauses).join(', '), + options = { + :select => @select_values.any? ? @select_values.join(", ") : nil, + :joins => arel.joins(arel), + :group => @group_values.any? ? @group_values.join(", ") : nil, :order => order_clause, :conditions => where_clause, - :limit => @relation.taken, - :offset => @relation.skipped, - :from => (@relation.send(:from_clauses) if @relation.send(:sources).present?) - }, - ActiveRecord::Associations::ClassMethods::JoinDependency.new(@klass, @eager_load_associations + @includes_associations, nil)) + :limit => arel.taken, + :offset => arel.skipped, + :from => (arel.send(:from_clauses) if arel.send(:sources).present?) + } + + including = (@eager_load_values + @includes_values).uniq + join_dependency = ActiveRecord::Associations::ClassMethods::JoinDependency.new(@klass, including, nil) + @klass.send(:find_with_associations, options, join_dependency) rescue ThrowResult [] end else - @klass.find_by_sql(@relation.to_sql) + @klass.find_by_sql(arel.to_sql) end - preload = @preload_associations - preload += @includes_associations unless find_with_associations + preload = @preload_values + preload += @includes_values unless find_with_associations preload.each {|associations| @klass.send(:preload_associations, @records, associations) } - @records.each { |record| record.readonly! } if @readonly + # @readonly_value is true only if set explicity. @implicit_readonly is true if there are JOINS and no explicit SELECT. + readonly = @readonly_value.nil? ? @implicit_readonly : @readonly_value + @records.each { |record| record.readonly! } if readonly @loaded = true @records @@ -97,7 +102,7 @@ module ActiveRecord if block_given? to_a.many? { |*block_args| yield(*block_args) } else - @relation.send(:taken).present? ? to_a.many? : size > 1 + arel.send(:taken).present? ? to_a.many? : size > 1 end end @@ -107,7 +112,7 @@ module ActiveRecord end def delete_all - @relation.delete.tap { reset } + arel.delete.tap { reset } end def delete(id_or_array) @@ -124,28 +129,33 @@ module ActiveRecord end def reset - @first = @last = @create_scope = @to_sql = @order_clause = nil + @first = @last = @to_sql = @order_clause = @scope_for_create = @arel = nil @records = [] self end - def table - @table ||= Arel::Table.new(@klass.table_name, :engine => @klass.active_relation_engine) - end - def primary_key @primary_key ||= table[@klass.primary_key] end def to_sql - @to_sql ||= @relation.to_sql + @to_sql ||= arel.to_sql + end + + def scope_for_create + @scope_for_create ||= begin + @create_with_value || wheres.inject({}) do |hash, where| + hash[where.operand1.name] = where.operand2.value if where.is_a?(Arel::Predicates::Equality) + hash + end + end end protected def method_missing(method, *args, &block) - if @relation.respond_to?(method) - @relation.send(method, *args, &block) + if arel.respond_to?(method) + arel.send(method, *args, &block) elsif Array.method_defined?(method) to_a.send(method, *args, &block) elsif match = DynamicFinderMatch.match(method) @@ -163,26 +173,19 @@ module ActiveRecord end 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 + @klass.send(:with_scope, :create => scope_for_create, :find => {}) { yield } end def where_clause(join_string = " AND ") - @relation.send(:where_clauses).join(join_string) + arel.send(:where_clauses).join(join_string) end def order_clause - @order_clause ||= @relation.send(:order_clauses).join(', ') + @order_clause ||= arel.send(:order_clauses).join(', ') end def references_eager_loaded_tables? - joined_tables = (tables_in_string(@relation.joins(relation)) + [table.name, table.table_alias]).compact.uniq + joined_tables = (tables_in_string(arel.joins(arel)) + [table.name, table.table_alias]).compact.uniq (tables_in_string(to_sql) - joined_tables).any? end diff --git a/activerecord/lib/active_record/relation/calculation_methods.rb b/activerecord/lib/active_record/relation/calculation_methods.rb index 5246c7bc5d..91de89e607 100644 --- a/activerecord/lib/active_record/relation/calculation_methods.rb +++ b/activerecord/lib/active_record/relation/calculation_methods.rb @@ -25,7 +25,7 @@ module ActiveRecord operation = operation.to_s.downcase if operation == "count" - joins = @relation.joins(relation) + joins = arel.joins(arel) if joins.present? && joins =~ /LEFT OUTER/i distinct = true column_name = @klass.primary_key if column_name == :all @@ -40,7 +40,7 @@ module ActiveRecord distinct = options[:distinct] || distinct column_name = :all if column_name.blank? && operation == "count" - if @relation.send(:groupings).any? + if @group_values.any? return execute_grouped_calculation(operation, column_name) else return execute_simple_calculation(operation, column_name, distinct) @@ -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.active_relation, column_name) + Arel::Attribute.new(@klass.unscoped, column_name) else Arel::SqlLiteral.new(column_name == :all ? "*" : column_name.to_s) end @@ -63,7 +63,7 @@ module ActiveRecord end def execute_grouped_calculation(operation, column_name) #:nodoc: - group_attr = @relation.send(:groupings).first.value + group_attr = @group_values.first association = @klass.reflect_on_association(group_attr.to_sym) associated = association && association.macro == :belongs_to # only count belongs_to associations group_field = associated ? association.primary_key_name : group_attr @@ -77,7 +77,7 @@ module ActiveRecord select_statement = if operation == 'count' && column_name == :all "COUNT(*) AS count_all" else - Arel::Attribute.new(@klass.active_relation, column_name).send(operation).as(aggregate_alias).to_sql + Arel::Attribute.new(@klass.unscoped, column_name).send(operation).as(aggregate_alias).to_sql end select_statement << ", #{group_field} AS #{group_alias}" @@ -106,7 +106,6 @@ module ActiveRecord column_name = :all # Handles count(), count(:column), count(:distinct => true), count(:column, :distinct => true) - # TODO : relation.projections only works when .select() was last in the chain. Fix it! case args.size when 0 select = get_projection_name_from_chained_relations @@ -165,12 +164,8 @@ module ActiveRecord column ? column.type_cast(value) : value end - def get_projection_name_from_chained_relations(relation = @relation) - if relation.respond_to?(:projections) && relation.projections.present? - relation.send(:select_clauses).join(', ') - elsif relation.respond_to?(:relation) - get_projection_name_from_chained_relations(relation.relation) - end + def get_projection_name_from_chained_relations + @select_values.join(", ") if @select_values.present? end end diff --git a/activerecord/lib/active_record/relation/finder_methods.rb b/activerecord/lib/active_record/relation/finder_methods.rb index c3e5f27838..3668b0997f 100644 --- a/activerecord/lib/active_record/relation/finder_methods.rb +++ b/activerecord/lib/active_record/relation/finder_methods.rb @@ -93,15 +93,15 @@ module ActiveRecord result = where(primary_key.in(ids)).all expected_size = - if @relation.taken && ids.size > @relation.taken - @relation.taken + if arel.taken && ids.size > arel.taken + arel.taken else ids.size end # 11 ids with limit 3, offset 9 should give 2 results. - if @relation.skipped && (ids.size - @relation.skipped < expected_size) - expected_size = ids.size - @relation.skipped + if arel.skipped && (ids.size - arel.skipped < expected_size) + expected_size = ids.size - arel.skipped end if result.size == expected_size diff --git a/activerecord/lib/active_record/relation/query_methods.rb b/activerecord/lib/active_record/relation/query_methods.rb index 525a9cb365..a3ac58bc81 100644 --- a/activerecord/lib/active_record/relation/query_methods.rb +++ b/activerecord/lib/active_record/relation/query_methods.rb @@ -1,72 +1,61 @@ module ActiveRecord module QueryMethods - - def preload(*associations) - spawn.tap {|r| r.preload_associations += Array.wrap(associations) } - end - - def includes(*associations) - spawn.tap {|r| r.includes_associations += Array.wrap(associations) } - end - - def eager_load(*associations) - spawn.tap {|r| r.eager_load_associations += Array.wrap(associations) } - end - - def readonly(status = true) - spawn.tap {|r| r.readonly = status } - end - - def select(selects) - if selects.present? - relation = spawn(@relation.project(selects)) - relation.readonly = @relation.joins(relation).present? ? false : @readonly - relation - else - spawn + extend ActiveSupport::Concern + + included do + (ActiveRecord::Relation::ASSOCIATION_METHODS + ActiveRecord::Relation::MULTI_VALUE_METHODS).each do |query_method| + attr_accessor :"#{query_method}_values" + + class_eval <<-CEVAL + def #{query_method}(*args) + spawn.tap do |new_relation| + new_relation.#{query_method}_values ||= [] + value = Array.wrap(args.flatten).reject {|x| x.blank? } + new_relation.#{query_method}_values += value if value.present? + end + end + CEVAL end - end - - def from(from) - from.present? ? spawn(@relation.from(from)) : spawn - end - def having(*args) - return spawn if args.blank? - - if [String, Hash, Array].include?(args.first.class) - havings = @klass.send(:merge_conditions, args.size > 1 ? Array.wrap(args) : args.first) - else - havings = args.first + [:where, :having].each do |query_method| + class_eval <<-CEVAL + def #{query_method}(*args) + spawn.tap do |new_relation| + new_relation.#{query_method}_values ||= [] + value = build_where(*args) + new_relation.#{query_method}_values += [*value] if value.present? + end + end + CEVAL end - spawn(@relation.having(havings)) - end + ActiveRecord::Relation::SINGLE_VALUE_METHODS.each do |query_method| + attr_accessor :"#{query_method}_value" - def group(groups) - groups.present? ? spawn(@relation.group(groups)) : spawn - end - - def order(orders) - orders.present? ? spawn(@relation.order(orders)) : spawn + class_eval <<-CEVAL + def #{query_method}(value = true) + spawn.tap do |new_relation| + new_relation.#{query_method}_value = value + end + end + CEVAL + end end def lock(locks = true) + relation = spawn case locks - when String - spawn(@relation.lock(locks)) - when TrueClass, NilClass - spawn(@relation.lock) + when String, TrueClass, NilClass + spawn.tap {|new_relation| new_relation.lock_value = locks || true } else - spawn + spawn.tap {|new_relation| new_relation.lock_value = false } end end def reverse_order - relation = spawn - relation.instance_variable_set(:@orders, nil) + order_clause = arel.send(:order_clauses).join(', ') + relation = except(:order) - order_clause = @relation.send(:order_clauses).join(', ') if order_clause.present? relation.order(reverse_sql_order(order_clause)) else @@ -74,41 +63,108 @@ module ActiveRecord end end - def limit(limits) - limits.present? ? spawn(@relation.take(limits)) : spawn + def arel + @arel ||= build_arel end - def offset(offsets) - offsets.present? ? spawn(@relation.skip(offsets)) : spawn - end + def build_arel + arel = table - def on(join) - spawn(@relation.on(join)) - end + joined_associations = [] + association_joins = [] - def joins(join, join_type = nil) - return spawn if join.blank? + joins = @joins_values.map {|j| j.respond_to?(:strip) ? j.strip : j}.uniq - join_relation = case join - when String - @relation.join(join) - when Hash, Array, Symbol - if @klass.send(:array_of_strings?, join) - @relation.join(join.join(' ')) + # Build association joins first + joins.each do |join| + association_joins << join if [Hash, Array, Symbol].include?(join.class) && !@klass.send(:array_of_strings?, join) + end + + if association_joins.any? + join_dependency = ActiveRecord::Associations::ClassMethods::JoinDependency.new(@klass, association_joins.uniq, nil) + to_join = [] + + join_dependency.join_associations.each do |association| + if (association_relation = association.relation).is_a?(Array) + to_join << [association_relation.first, association.association_join.first] + to_join << [association_relation.last, association.association_join.last] + else + to_join << [association_relation, association.association_join] + end + end + + to_join.each do |tj| + unless joined_associations.detect {|ja| ja[0] == tj[0] && ja[1] == tj[1] } + joined_associations << tj + arel = arel.join(tj[0]).on(*tj[1]) + end + end + end + + joins.each do |join| + next if join.blank? + + @implicit_readonly = true + + case join + when Relation::JoinOperation + arel = arel.join(join.relation, join.join_class).on(*join.on) + when Hash, Array, Symbol + if @klass.send(:array_of_strings?, join) + join_string = join.join(' ') + arel = arel.join(join_string) + end else - @relation.join(@klass.send(:build_association_joins, join)) + arel = arel.join(join) end - else - @relation.join(join, join_type) end - spawn(join_relation).tap { |r| r.readonly = true } + @where_values.uniq.each do |w| + arel = w.is_a?(String) ? arel.where(w) : arel.where(*w) + end + + @having_values.uniq.each do |h| + arel = h.is_a?(String) ? arel.having(h) : arel.having(*h) + end + + arel = arel.take(@limit_value) if @limit_value.present? + arel = arel.skip(@offset_value) if @offset_value.present? + + @group_values.uniq.each do |g| + arel = arel.group(g) if g.present? + end + + @order_values.uniq.each do |o| + arel = arel.order(o) if o.present? + end + + selects = @select_values.uniq + + if selects.present? + selects.each do |s| + @implicit_readonly = false + arel = arel.project(s) if s.present? + end + elsif joins.present? + arel = arel.project(@klass.quoted_table_name + '.*') + end + + arel = arel.from(@from_value) if @from_value.present? + + case @lock_value + when TrueClass + arel = arel.lock + when String + arel = arel.lock(@lock_value) + end + + arel end - def where(*args) - return spawn if args.blank? + def build_where(*args) + return if args.blank? - builder = PredicateBuilder.new(Arel::Sql::Engine.new(@klass)) + builder = PredicateBuilder.new(table.engine) conditions = if [String, Array].include?(args.first.class) merged = @klass.send(:merge_conditions, args.size > 1 ? Array.wrap(args) : args.first) @@ -120,7 +176,7 @@ module ActiveRecord args.first end - conditions.is_a?(String) ? spawn(@relation.where(conditions)) : spawn(@relation.where(*conditions)) + conditions end private diff --git a/activerecord/lib/active_record/relation/spawn_methods.rb b/activerecord/lib/active_record/relation/spawn_methods.rb index a637e97155..f4abaae43e 100644 --- a/activerecord/lib/active_record/relation/spawn_methods.rb +++ b/activerecord/lib/active_record/relation/spawn_methods.rb @@ -1,75 +1,119 @@ module ActiveRecord module SpawnMethods - def spawn(relation = @relation) - relation = Relation.new(@klass, relation) - relation.readonly = @readonly - relation.preload_associations = @preload_associations - relation.eager_load_associations = @eager_load_associations - relation.includes_associations = @includes_associations - relation.table = table + def spawn(arel_table = self.table) + relation = Relation.new(@klass, arel_table) + + (Relation::ASSOCIATION_METHODS + Relation::MULTI_VALUE_METHODS).each do |query_method| + relation.send(:"#{query_method}_values=", send(:"#{query_method}_values")) + end + + Relation::SINGLE_VALUE_METHODS.each do |query_method| + relation.send(:"#{query_method}_value=", send(:"#{query_method}_value")) + end + relation end def merge(r) - raise ArgumentError, "Cannot merge a #{r.klass.name} relation with #{@klass.name} relation" if r.klass != @klass - - merged_relation = spawn(table).eager_load(r.eager_load_associations).preload(r.preload_associations).includes(r.includes_associations) - merged_relation.readonly = r.readonly - - [self.relation, r.relation].each do |arel| - merged_relation = merged_relation. - joins(arel.joins(arel)). - group(arel.groupings). - limit(arel.taken). - offset(arel.skipped). - select(arel.send(:select_clauses)). - from(arel.sources). - having(arel.havings). - lock(arel.locked) + if r.klass != @klass + raise ArgumentError, "Cannot merge a #{r.klass.name}(##{r.klass.object_id}) relation with #{@klass.name}(##{@klass.object_id}) relation" end - relation_order = r.send(:order_clause) - merged_order = relation_order.present? ? relation_order : order_clause - merged_relation = merged_relation.order(merged_order) + merged_relation = spawn.eager_load(r.eager_load_values).preload(r.preload_values).includes(r.includes_values) + + merged_relation.readonly_value = r.readonly_value unless r.readonly_value.nil? + merged_relation.limit_value = r.limit_value if r.limit_value.present? + merged_relation.lock_value = r.lock_value unless merged_relation.lock_value + merged_relation.offset_value = r.offset_value if r.offset_value.present? + + merged_relation = merged_relation. + joins(r.joins_values). + group(r.group_values). + select(r.select_values). + from(r.from_value). + having(r.having_values) - merged_wheres = @relation.wheres + merged_relation.order_values = Array.wrap(order_values) + Array.wrap(r.order_values) - r.wheres.each do |w| + merged_relation.create_with_value = @create_with_value + + if @create_with_value && r.create_with_value + merged_relation.create_with_value = @create_with_value.merge(r.create_with_value) + else + merged_relation.create_with_value = r.create_with_value || @create_with_value + end + + merged_wheres = @where_values + + r.where_values.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 + merged_wheres += [w] end - merged_relation.where(*merged_wheres) + merged_relation.where_values = merged_wheres + + merged_relation end alias :& :merge def except(*skips) result = Relation.new(@klass, table) - result.table = table - [:eager_load, :preload, :includes].each do |load_method| - result = result.send(load_method, send(:"#{load_method}_associations")) + (Relation::ASSOCIATION_METHODS + Relation::MULTI_VALUE_METHODS).each do |method| + result.send(:"#{method}_values=", send(:"#{method}_values")) unless skips.include?(method) + end + + Relation::SINGLE_VALUE_METHODS.each do |method| + result.send(:"#{method}_value=", send(:"#{method}_value")) unless skips.include?(method) end - result.readonly = self.readonly unless skips.include?(:readonly) + result + end - result = result.joins(@relation.joins(@relation)) unless skips.include?(:joins) - result = result.group(@relation.groupings) unless skips.include?(:group) - result = result.limit(@relation.taken) unless skips.include?(:limit) - result = result.offset(@relation.skipped) unless skips.include?(:offset) - result = result.select(@relation.send(:select_clauses)) unless skips.include?(:select) - result = result.from(@relation.sources) unless skips.include?(:from) - result = result.order(order_clause) unless skips.include?(:order) - result = result.where(*@relation.wheres) unless skips.include?(:where) - result = result.having(*@relation.havings) unless skips.include?(:having) - result = result.lock(@relation.locked) unless skips.include?(:lock) + def only(*onlies) + result = Relation.new(@klass, table) + + onlies.each do |only| + if (Relation::ASSOCIATION_METHODS + Relation::MULTI_VALUE_METHODS).include?(only) + result.send(:"#{only}_values=", send(:"#{only}_values")) + elsif Relation::SINGLE_VALUE_METHODS.include?(only) + result.send(:"#{only}_value=", send(:"#{only}_value")) + else + raise "Invalid argument : #{only}" + end + end result end + VALID_FIND_OPTIONS = [ :conditions, :include, :joins, :limit, :offset, + :order, :select, :readonly, :group, :having, :from, :lock ] + + def apply_finder_options(options) + options.assert_valid_keys(VALID_FIND_OPTIONS) + + relation = spawn + + relation = relation.joins(options[:joins]). + where(options[:conditions]). + select(options[:select]). + group(options[:group]). + having(options[:having]). + order(options[:order]). + limit(options[:limit]). + offset(options[:offset]). + from(options[:from]). + includes(options[:include]) + + relation = relation.lock(options[:lock]) if options[:lock].present? + relation = relation.readonly(options[:readonly]) if options.has_key?(:readonly) + + relation + end + end end diff --git a/activerecord/lib/active_record/schema.rb b/activerecord/lib/active_record/schema.rb index 8a32cf1ca2..a996a0ebac 100644 --- a/activerecord/lib/active_record/schema.rb +++ b/activerecord/lib/active_record/schema.rb @@ -28,6 +28,10 @@ module ActiveRecord class Schema < Migration private_class_method :new + def self.migrations_path + ActiveRecord::Migrator.migrations_path + end + # Eval the given block. All methods available to the current connection # adapter are available within the block, so you can easily use the # database definition DSL to build up your schema (+create_table+, @@ -44,7 +48,7 @@ module ActiveRecord unless info[:version].blank? initialize_schema_migrations_table - assume_migrated_upto_version info[:version] + assume_migrated_upto_version(info[:version], migrations_path) end end end diff --git a/activerecord/lib/active_record/test_case.rb b/activerecord/lib/active_record/test_case.rb index 2dfe2c09ea..0a77ad5fd7 100644 --- a/activerecord/lib/active_record/test_case.rb +++ b/activerecord/lib/active_record/test_case.rb @@ -1,5 +1,3 @@ -require "active_support/test_case" - module ActiveRecord class TestCase < ActiveSupport::TestCase #:nodoc: def assert_date_from_db(expected, actual, message = nil) diff --git a/activerecord/lib/active_record/validations.rb b/activerecord/lib/active_record/validations.rb index 12c1f23763..d5adcba3ba 100644 --- a/activerecord/lib/active_record/validations.rb +++ b/activerecord/lib/active_record/validations.rb @@ -11,7 +11,7 @@ module ActiveRecord def initialize(record) @record = record errors = @record.errors.full_messages.join(I18n.t('support.array.words_connector', :default => ', ')) - super(I18n.t('activerecord.errors.messages.record_invalid', :errors => errors)) + super(I18n.t('errors.messages.record_invalid', :errors => errors)) end end diff --git a/activerecord/lib/active_record/validations/associated.rb b/activerecord/lib/active_record/validations/associated.rb index 66b78682ad..e41635134c 100644 --- a/activerecord/lib/active_record/validations/associated.rb +++ b/activerecord/lib/active_record/validations/associated.rb @@ -40,8 +40,7 @@ module ActiveRecord # not occur (e.g. <tt>:unless => :skip_validation</tt>, or <tt>:unless => Proc.new { |user| user.signup_step <= 2 }</tt>). The # method, proc or string should return or evaluate to a true or false value. def validates_associated(*attr_names) - options = attr_names.extract_options! - validates_with AssociatedValidator, options.merge(:attributes => attr_names) + validates_with AssociatedValidator, _merge_attributes(attr_names) end end end diff --git a/activerecord/lib/active_record/validations/uniqueness.rb b/activerecord/lib/active_record/validations/uniqueness.rb index 7efd312357..e28808df98 100644 --- a/activerecord/lib/active_record/validations/uniqueness.rb +++ b/activerecord/lib/active_record/validations/uniqueness.rb @@ -2,13 +2,17 @@ module ActiveRecord module Validations class UniquenessValidator < ActiveModel::EachValidator def initialize(options) - @klass = options.delete(:klass) super(options.reverse_merge(:case_sensitive => true)) end + # Unfortunately, we have to tie Uniqueness validators to a class. + def setup(klass) + @klass = klass + end + def validate_each(record, attribute, value) finder_class = find_finder_class_for(record) - table = finder_class.active_relation + table = finder_class.unscoped table_name = record.class.quoted_table_name sql, params = mount_sql_and_params(finder_class, table_name, attribute, value) @@ -170,8 +174,7 @@ module ActiveRecord # such a case. # def validates_uniqueness_of(*attr_names) - options = attr_names.extract_options! - validates_with UniquenessValidator, options.merge(:attributes => attr_names, :klass => self) + validates_with UniquenessValidator, _merge_attributes(attr_names) end end end |