diff options
author | Emilio Tagua <miloops@gmail.com> | 2011-02-15 12:01:04 -0300 |
---|---|---|
committer | Emilio Tagua <miloops@gmail.com> | 2011-02-15 12:01:04 -0300 |
commit | 8ee0b4414890f919594c1a388d987b5b7364a505 (patch) | |
tree | 6c6c6aa19da0eb8066d2f0b9b02b08f2cc696c29 /activerecord/lib/active_record | |
parent | 348c0ec7c656b3691aa4e687565d28259ca0f693 (diff) | |
parent | c9f1ab5365319e087e1b010a3f90626a2b8f080b (diff) | |
download | rails-8ee0b4414890f919594c1a388d987b5b7364a505.tar.gz rails-8ee0b4414890f919594c1a388d987b5b7364a505.tar.bz2 rails-8ee0b4414890f919594c1a388d987b5b7364a505.zip |
Merge remote branch 'rails/master' into identity_map
Conflicts:
activerecord/examples/performance.rb
activerecord/lib/active_record/association_preload.rb
activerecord/lib/active_record/associations.rb
activerecord/lib/active_record/associations/association_proxy.rb
activerecord/lib/active_record/autosave_association.rb
activerecord/lib/active_record/base.rb
activerecord/lib/active_record/nested_attributes.rb
activerecord/test/cases/relations_test.rb
Diffstat (limited to 'activerecord/lib/active_record')
60 files changed, 3041 insertions, 2156 deletions
diff --git a/activerecord/lib/active_record/aggregations.rb b/activerecord/lib/active_record/aggregations.rb index 8cd7389005..90d3b58c78 100644 --- a/activerecord/lib/active_record/aggregations.rb +++ b/activerecord/lib/active_record/aggregations.rb @@ -4,9 +4,7 @@ module ActiveRecord extend ActiveSupport::Concern def clear_aggregation_cache #:nodoc: - self.class.reflect_on_all_aggregations.to_a.each do |assoc| - instance_variable_set "@#{assoc.name}", nil - end if self.persisted? + @aggregation_cache.clear if persisted? end # Active Record implements aggregation through a macro-like class method called +composed_of+ @@ -222,53 +220,32 @@ module ActiveRecord private def reader_method(name, class_name, mapping, allow_nil, constructor) - module_eval do - define_method(name) do |*args| - force_reload = args.first || false - - unless instance_variable_defined?("@#{name}") - instance_variable_set("@#{name}", nil) - end - - if (instance_variable_get("@#{name}").nil? || force_reload) && (!allow_nil || mapping.any? {|pair| !read_attribute(pair.first).nil? }) - attrs = mapping.collect {|pair| read_attribute(pair.first)} - object = case constructor - when Symbol - class_name.constantize.send(constructor, *attrs) - when Proc, Method - constructor.call(*attrs) - else - raise ArgumentError, 'Constructor must be a symbol denoting the constructor method to call or a Proc to be invoked.' - end - instance_variable_set("@#{name}", object) - end - instance_variable_get("@#{name}") + define_method(name) do + if @aggregation_cache[name].nil? && (!allow_nil || mapping.any? {|pair| !read_attribute(pair.first).nil? }) + attrs = mapping.collect {|pair| read_attribute(pair.first)} + object = constructor.respond_to?(:call) ? + constructor.call(*attrs) : + class_name.constantize.send(constructor, *attrs) + @aggregation_cache[name] = object end + @aggregation_cache[name] end - end def writer_method(name, class_name, mapping, allow_nil, converter) - module_eval do - define_method("#{name}=") do |part| - if part.nil? && allow_nil - mapping.each { |pair| self[pair.first] = nil } - instance_variable_set("@#{name}", nil) - else - unless part.is_a?(class_name.constantize) || converter.nil? - part = case converter - when Symbol - class_name.constantize.send(converter, part) - when Proc, Method - converter.call(part) - else - raise ArgumentError, 'Converter must be a symbol denoting the converter method to call or a Proc to be invoked.' - end - end - - mapping.each { |pair| self[pair.first] = part.send(pair.last) } - instance_variable_set("@#{name}", part.freeze) + define_method("#{name}=") do |part| + if part.nil? && allow_nil + mapping.each { |pair| self[pair.first] = nil } + @aggregation_cache[name] = nil + else + unless part.is_a?(class_name.constantize) || converter.nil? + part = converter.respond_to?(:call) ? + converter.call(part) : + class_name.constantize.send(converter, part) end + + mapping.each { |pair| self[pair.first] = part.send(pair.last) } + @aggregation_cache[name] = part.freeze end end end diff --git a/activerecord/lib/active_record/association_preload.rb b/activerecord/lib/active_record/association_preload.rb index 373532704f..52d2c49836 100644 --- a/activerecord/lib/active_record/association_preload.rb +++ b/activerecord/lib/active_record/association_preload.rb @@ -125,45 +125,48 @@ module ActiveRecord def add_preloaded_records_to_collection(parent_records, reflection_name, associated_record) parent_records.each do |parent_record| association_proxy = parent_record.send(reflection_name) - association_proxy.loaded - association_proxy.target = association_proxy.target | [*Array.wrap(associated_record)] - - association_proxy.__send__(:set_inverse_instance, associated_record, parent_record) + association_proxy.loaded! + association_proxy.target.concat(Array.wrap(associated_record)) + association_proxy.send(:set_inverse_instance, associated_record) end end def add_preloaded_record_to_collection(parent_records, reflection_name, associated_record) parent_records.each do |parent_record| - parent_record.send("set_#{reflection_name}_target", associated_record) + parent_record.send(:association_proxy, reflection_name).target = associated_record end end - def set_association_collection_records(id_to_record_map, reflection_name, associated_records, key) + def set_association_collection_records(id_to_parent_map, reflection_name, associated_records, key) associated_records.each do |associated_record| - mapped_records = id_to_record_map[associated_record[key].to_s] - add_preloaded_records_to_collection(mapped_records, reflection_name, associated_record) + parent_records = id_to_parent_map[associated_record[key].to_s] + add_preloaded_records_to_collection(parent_records, reflection_name, associated_record) end end def set_association_single_records(id_to_record_map, reflection_name, associated_records, key) seen_keys = {} associated_records.each do |associated_record| + seen_key = associated_record[key].to_s + #this is a has_one or belongs_to: there should only be one record. #Unfortunately we can't (in portable way) ask the database for #'all records where foo_id in (x,y,z), but please # only one row per distinct foo_id' so this where we enforce that - next if seen_keys[associated_record[key].to_s] - seen_keys[associated_record[key].to_s] = true - mapped_records = id_to_record_map[associated_record[key].to_s] + next if seen_keys.key? seen_key + + seen_keys[seen_key] = true + mapped_records = id_to_record_map[seen_key] mapped_records.each do |mapped_record| - association_proxy = mapped_record.send("set_#{reflection_name}_target", associated_record) - association_proxy.__send__(:set_inverse_instance, associated_record, mapped_record) + association_proxy = mapped_record.send(:association_proxy, reflection_name) + association_proxy.target = associated_record + association_proxy.send(:set_inverse_instance, associated_record) end end id_to_record_map.each do |id, records| - next if seen_keys.include?(id.to_s) - records.each {|record| record.send("set_#{reflection_name}_target", nil) } + next if seen_keys.include?(id) + add_preloaded_record_to_collection(records, reflection_name, nil) end end @@ -172,57 +175,80 @@ module ActiveRecord # <tt>(id_to_record_map, ids)</tt> where +id_to_record_map+ is the Hash, # and +ids+ is an Array of record IDs. def construct_id_map(records, primary_key=nil) - id_to_record_map = {} - ids = [] - records.each do |record| + records.group_by do |record| primary_key ||= record.class.primary_key - ids << record[primary_key] - mapped_records = (id_to_record_map[ids.last.to_s] ||= []) - mapped_records << record + record[primary_key].to_s end - ids.uniq! - return id_to_record_map, ids end def preload_has_and_belongs_to_many_association(records, reflection, preload_options={}) - table_name = reflection.klass.quoted_table_name - id_to_record_map, ids = construct_id_map(records) - records.each {|record| record.send(reflection.name).loaded} + + left = reflection.klass.arel_table + + id_to_record_map = construct_id_map(records) + + records.each { |record| record.send(reflection.name).loaded! } options = reflection.options - conditions = "t0.#{reflection.primary_key_name} #{in_or_equals_for_ids(ids)}" - conditions << append_conditions(reflection, preload_options) + right = Arel::Table.new(options[:join_table]).alias('t0') + + join_condition = left[reflection.klass.primary_key].eq( + right[reflection.association_foreign_key]) + + join = left.create_join(right, left.create_on(join_condition)) + select = [ + # FIXME: options[:select] is always nil in the tests. Do we really + # need it? + options[:select] || left[Arel.star], + right[reflection.foreign_key].as( + Arel.sql('the_parent_record_id')) + ] associated_records_proxy = reflection.klass.unscoped. 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]) - all_associated_records = ActiveRecord::IdentityMap.without do - associated_records(ids) do |some_ids| - associated_records_proxy.where([conditions, ids]).to_a - end - end + associated_records_proxy.joins_values = [join] + associated_records_proxy.select_values = select + + custom_conditions = append_conditions(reflection, preload_options) + + klass = associated_records_proxy.klass - set_association_collection_records(id_to_record_map, reflection.name, all_associated_records, 'the_parent_record_id') + associated_records(id_to_record_map.keys) { |some_ids| + method = in_or_equal(some_ids) + conditions = right.create_and( + [right[reflection.foreign_key].send(*method)] + + custom_conditions) + + relation = associated_records_proxy.where(conditions) + klass.connection.select_all(relation.arel.to_sql, 'SQL', relation.bind_values) + }.map! { |row| + parent_records = id_to_record_map[row['the_parent_record_id'].to_s] + associated_record = klass.instantiate row + add_preloaded_records_to_collection( + parent_records, reflection.name, associated_record) + associated_record + } end def preload_has_one_association(records, reflection, preload_options={}) - return if records.first.send("loaded_#{reflection.name}?") - id_to_record_map, ids = construct_id_map(records, reflection.options[:primary_key]) + return if records.first.send(:association_proxy, reflection.name).loaded? + id_to_record_map = construct_id_map(records, reflection.options[:primary_key]) options = reflection.options - records.each {|record| record.send("set_#{reflection.name}_target", nil)} + + add_preloaded_record_to_collection(records, reflection.name, nil) + if options[:through] through_records = preload_through_records(records, reflection, options[:through]) unless through_records.empty? through_reflection = reflections[options[:through]] - through_primary_key = through_reflection.primary_key_name + through_primary_key = through_reflection.foreign_key source = reflection.source_reflection.name through_records.first.class.preload_associations(through_records, source) if through_reflection.macro == :belongs_to - id_to_record_map = construct_id_map(records, through_primary_key).first + id_to_record_map = construct_id_map(records, through_primary_key) through_primary_key = through_reflection.klass.primary_key end @@ -232,7 +258,7 @@ module ActiveRecord end end else - set_association_single_records(id_to_record_map, reflection.name, find_associated_records(ids, reflection, preload_options), reflection.primary_key_name) + set_association_single_records(id_to_record_map, reflection.name, find_associated_records(id_to_record_map.keys, reflection, preload_options), reflection.foreign_key) end end @@ -240,9 +266,9 @@ module ActiveRecord return if records.first.send(reflection.name).loaded? options = reflection.options - primary_key_name = reflection.through_reflection_primary_key_name - id_to_record_map, ids = construct_id_map(records, primary_key_name || reflection.options[:primary_key]) - records.each {|record| record.send(reflection.name).loaded} + foreign_key = reflection.through_reflection_foreign_key + id_to_record_map = construct_id_map(records, foreign_key || reflection.options[:primary_key]) + records.each { |record| record.send(reflection.name).loaded! } if options[:through] through_records = preload_through_records(records, reflection, options[:through]) @@ -257,14 +283,14 @@ module ActiveRecord end else - set_association_collection_records(id_to_record_map, reflection.name, find_associated_records(ids, reflection, preload_options), - reflection.primary_key_name) + set_association_collection_records(id_to_record_map, reflection.name, find_associated_records(id_to_record_map.keys, reflection, preload_options), + reflection.foreign_key) end end def preload_through_records(records, reflection, through_association) if reflection.options[:source_type] - interface = reflection.source_reflection.options[:foreign_type] + interface = reflection.source_reflection.foreign_type preload_options = {:conditions => ["#{connection.quote_column_name interface} = ?", reflection.options[:source_type]]} records.compact! @@ -294,76 +320,71 @@ module ActiveRecord end def preload_belongs_to_association(records, reflection, preload_options={}) - return if records.first.send("loaded_#{reflection.name}?") + return if records.first.send(:association_proxy, reflection.name).loaded? options = reflection.options - primary_key_name = reflection.primary_key_name klasses_and_ids = {} if options[:polymorphic] - polymorph_type = options[:foreign_type] - # Construct a mapping from klass to a list of ids to load and a mapping of those ids back # to their parent_records records.each do |record| - if klass = record.send(polymorph_type) - klass_id = record.send(primary_key_name) + if klass = record.send(reflection.foreign_type) + klass_id = record.send(reflection.foreign_key) if klass_id - id_map = klasses_and_ids[klass] ||= {} + id_map = klasses_and_ids[klass.constantize] ||= {} (id_map[klass_id.to_s] ||= []) << record end end end else - id_map = {} - records.each do |record| - key = record.send(primary_key_name) - (id_map[key.to_s] ||= []) << record if key + id_map = records.group_by do |record| + key = record.send(reflection.foreign_key) + key && key.to_s end - klasses_and_ids[reflection.klass.name] = id_map unless id_map.empty? + klasses_and_ids[reflection.klass] = id_map unless id_map.empty? end - klasses_and_ids.each do |klass_name, _id_map| - klass = klass_name.constantize - - table_name = klass.quoted_table_name + klasses_and_ids.each do |klass, _id_map| primary_key = (reflection.options[:primary_key] || klass.primary_key).to_s - 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 - elsif column_type == :float - id.to_f - else - id - end - end + keys = _id_map.keys.compact - conditions = "#{table_name}.#{connection.quote_column_name(primary_key)} #{in_or_equals_for_ids(ids)}" - conditions << append_conditions(reflection, preload_options) + unless keys.empty? + table = klass.arel_table + method = in_or_equal(keys) + conditions = table[primary_key].send(*method) - associated_records = klass.unscoped.where([conditions, ids]).apply_finder_options(options.slice(:include, :select, :joins, :order)).to_a + custom_conditions = append_conditions(reflection, preload_options) + conditions = custom_conditions.inject(conditions) do |ast, cond| + ast.and cond + end + + associated_records = klass.unscoped.where(conditions).apply_finder_options(options.slice(:include, :select, :joins, :order)).to_a + else + associated_records = [] + end set_association_single_records(_id_map, reflection.name, associated_records, primary_key) end end def find_associated_records(ids, reflection, preload_options) - options = reflection.options - table_name = reflection.klass.quoted_table_name + options = reflection.options + table = reflection.klass.arel_table + + conditions = [] + + key = reflection.foreign_key if interface = reflection.options[:as] - conditions = "#{reflection.klass.quoted_table_name}.#{connection.quote_column_name "#{interface}_id"} #{in_or_equals_for_ids(ids)} and #{reflection.klass.quoted_table_name}.#{connection.quote_column_name "#{interface}_type"} = '#{self.base_class.sti_name}'" - else - foreign_key = reflection.primary_key_name - conditions = "#{reflection.klass.quoted_table_name}.#{foreign_key} #{in_or_equals_for_ids(ids)}" + key = "#{interface}_id" + conditions << table["#{interface}_type"].eq(base_class.sti_name) end - conditions << append_conditions(reflection, preload_options) + conditions.concat append_conditions(reflection, preload_options) find_options = { - :select => preload_options[:select] || options[:select] || Arel::SqlLiteral.new("#{table_name}.*"), + :select => preload_options[:select] || options[:select] || table[Arel.star], :include => preload_options[:include] || options[:include], :joins => options[:joins], :group => preload_options[:group] || options[:group], @@ -371,24 +392,30 @@ module ActiveRecord } associated_records(ids) do |some_ids| - reflection.klass.scoped.apply_finder_options(find_options.merge(:conditions => [conditions, some_ids])).to_a + method = in_or_equal(some_ids) + where = table.create_and(conditions + [table[key].send(*method)]) + + reflection.klass.scoped.apply_finder_options(find_options.merge(:conditions => where)).to_a end end + def process_conditions(conditions, klass = self) + if conditions.respond_to?(:to_proc) + conditions = instance_eval(&conditions) + end - def interpolate_sql_for_preload(sql) - instance_eval("%@#{sql.gsub('@', '\@')}@", __FILE__, __LINE__) + klass.send(:sanitize_sql, conditions) end def append_conditions(reflection, preload_options) - sql = "" - sql << " AND (#{interpolate_sql_for_preload(reflection.sanitized_conditions)})" if reflection.sanitized_conditions - sql << " AND (#{sanitize_sql preload_options[:conditions]})" if preload_options[:conditions] - sql + [ + ('(' + process_conditions(reflection.options[:conditions], reflection.klass) + ')' if reflection.options[:conditions]), + ('(' + process_conditions(preload_options[:conditions]) + ')' if preload_options[:conditions]), + ].compact.map { |x| Arel.sql x } end - def in_or_equals_for_ids(ids) - ids.size > 1 ? "IN (?)" : "= ?" + def in_or_equal(ids) + ids.length == 1 ? ['eq', ids.first] : ['in', ids] end # Some databases impose a limit on the number of ids in a list (in Oracle its 1000) @@ -397,7 +424,7 @@ module ActiveRecord in_clause_length = connection.in_clause_length || ids.size records = [] ids.each_slice(in_clause_length) do |some_ids| - records += yield(some_ids) + records.concat yield(some_ids) end records end diff --git a/activerecord/lib/active_record/associations.rb b/activerecord/lib/active_record/associations.rb index 371dad9104..a2db592c7d 100644 --- a/activerecord/lib/active_record/associations.rb +++ b/activerecord/lib/active_record/associations.rb @@ -20,18 +20,30 @@ module ActiveRecord end end - class HasManyThroughAssociationPolymorphicError < ActiveRecordError #:nodoc: + class HasManyThroughAssociationPolymorphicSourceError < ActiveRecordError #:nodoc: def initialize(owner_class_name, reflection, source_reflection) super("Cannot have a has_many :through association '#{owner_class_name}##{reflection.name}' on the polymorphic object '#{source_reflection.class_name}##{source_reflection.name}'.") end end + class HasManyThroughAssociationPolymorphicThroughError < ActiveRecordError #:nodoc: + def initialize(owner_class_name, reflection) + super("Cannot have a has_many :through association '#{owner_class_name}##{reflection.name}' which goes through the polymorphic association '#{owner_class_name}##{reflection.through_reflection.name}'.") + end + end + class HasManyThroughAssociationPointlessSourceTypeError < ActiveRecordError #:nodoc: def initialize(owner_class_name, reflection, source_reflection) super("Cannot have a has_many :through association '#{owner_class_name}##{reflection.name}' with a :source_type option if the '#{reflection.through_reflection.class_name}##{source_reflection.name}' is not polymorphic. Try removing :source_type on your association.") end end + class HasOneThroughCantAssociateThroughCollection < ActiveRecordError #:nodoc: + def initialize(owner_class_name, reflection, through_reflection) + super("Cannot have a has_one :through association '#{owner_class_name}##{reflection.name}' where the :through association '#{owner_class_name}##{through_reflection.name}' is a collection. Specify a has_one or belongs_to association in the :through option instead.") + end + end + class HasManyThroughSourceAssociationNotFoundError < ActiveRecordError #:nodoc: def initialize(reflection) through_reflection = reflection.through_reflection @@ -107,7 +119,9 @@ module ActiveRecord # These classes will be loaded when associations are created. # So there is no need to eager load them. autoload :AssociationCollection, 'active_record/associations/association_collection' + autoload :SingularAssociation, 'active_record/associations/singular_association' autoload :AssociationProxy, 'active_record/associations/association_proxy' + autoload :ThroughAssociation, 'active_record/associations/through_association' autoload :BelongsToAssociation, 'active_record/associations/belongs_to_association' autoload :BelongsToPolymorphicAssociation, 'active_record/associations/belongs_to_polymorphic_association' autoload :HasAndBelongsToManyAssociation, 'active_record/associations/has_and_belongs_to_many_association' @@ -118,30 +132,40 @@ module ActiveRecord # Clears out the association cache. def clear_association_cache #:nodoc: - self.class.reflect_on_all_associations.to_a.each do |assoc| - if IdentityMap.enabled? && instance_variable_defined?("@#{assoc.name}") - Array(instance_variable_get("@#{assoc.name}")).each do |t| - next unless t.respond_to?(:target) - IdentityMap.remove t.target unless t.target.nil? - end - end - instance_variable_set "@#{assoc.name}", nil - end if self.persisted? + @association_cache.clear if persisted? end + # :nodoc: + attr_reader :association_cache + + protected + + # Returns the proxy for the given association name, instantiating it if it doesn't + # already exist + def association_proxy(name) + association = association_instance_get(name) + + if association.nil? + reflection = self.class.reflect_on_association(name) + association = reflection.proxy_class.new(self, reflection) + association_instance_set(name, association) + end + + association + end + private # Returns the specified association instance if it responds to :loaded?, nil otherwise. def association_instance_get(name) - ivar = "@#{name}" - if instance_variable_defined?(ivar) - association = instance_variable_get(ivar) + if @association_cache.key? name + association = @association_cache[name] association if association.respond_to?(:loaded?) end end # Set the specified association instance. def association_instance_set(name, association) - instance_variable_set("@#{name}", association) + @association_cache[name] = association end # Associations are a set of macro-like class methods for tying objects together through @@ -185,7 +209,7 @@ module ActiveRecord # other=(other) | X | X | X # build_other(attributes={}) | X | | X # create_other(attributes={}) | X | | X - # other.create!(attributes={}) | | | X + # create_other!(attributes={}) | X | | X # # ===Collection associations (one-to-many / many-to-many) # | | | has_many @@ -208,10 +232,9 @@ module ActiveRecord # others.empty? | X | X | X # others.clear | X | X | X # others.delete(other,other,...) | X | X | X - # others.delete_all | X | X | + # others.delete_all | X | X | X # others.destroy_all | X | X | X # others.find(*args) | X | X | X - # others.find_first | X | | # others.exists? | X | X | X # others.uniq | X | X | X # others.reset | X | X | X @@ -325,26 +348,31 @@ module ActiveRecord # === One-to-one associations # # * Assigning an object to a +has_one+ association automatically saves that object and - # the object being replaced (if there is one), in order to update their primary - # keys - except if the parent object is unsaved (<tt>new_record? == true</tt>). - # * If either of these saves fail (due to one of the objects being invalid) the assignment - # statement returns +false+ and the assignment is cancelled. + # the object being replaced (if there is one), in order to update their foreign + # keys - except if the parent object is unsaved (<tt>new_record? == true</tt>). + # * If either of these saves fail (due to one of the objects being invalid), an + # <tt>ActiveRecord::RecordNotSaved</tt> exception is raised and the assignment is + # cancelled. # * If you wish to assign an object to a +has_one+ association without saving it, - # use the <tt>association.build</tt> method (documented below). + # use the <tt>build_association</tt> method (documented below). The object being + # replaced will still be saved to update its foreign key. # * Assigning an object to a +belongs_to+ association does not save the object, since - # the foreign key field belongs on the parent. It does not save the parent either. + # the foreign key field belongs on the parent. It does not save the parent either. # # === Collections # # * Adding an object to a collection (+has_many+ or +has_and_belongs_to_many+) automatically - # saves that object, except if the parent object (the owner of the collection) is not yet - # stored in the database. + # saves that object, except if the parent object (the owner of the collection) is not yet + # stored in the database. # * If saving any of the objects being added to a collection (via <tt>push</tt> or similar) - # fails, then <tt>push</tt> returns +false+. + # fails, then <tt>push</tt> returns +false+. + # * If saving fails while replacing the collection (via <tt>association=</tt>), an + # <tt>ActiveRecord::RecordNotSaved</tt> exception is raised and the assignment is + # cancelled. # * You can add an object to a collection without automatically saving it by using the - # <tt>collection.build</tt> method (documented below). + # <tt>collection.build</tt> method (documented below). # * All unsaved (<tt>new_record? == true</tt>) members of the collection are automatically - # saved when the parent is saved. + # saved when the parent is saved. # # === Association callbacks # @@ -804,6 +832,73 @@ module ActiveRecord # * does not work with <tt>:polymorphic</tt> associations. # * for +belongs_to+ associations +has_many+ inverse associations are ignored. # + # == Deleting from associations + # + # === Dependent associations + # + # +has_many+, +has_one+ and +belongs_to+ associations support the <tt>:dependent</tt> option. + # This allows you to specify that associated records should be deleted when the owner is + # deleted. + # + # For example: + # + # class Author + # has_many :posts, :dependent => :destroy + # end + # Author.find(1).destroy # => Will destroy all of the author's posts, too + # + # The <tt>:dependent</tt> option can have different values which specify how the deletion + # is done. For more information, see the documentation for this option on the different + # specific association types. + # + # === Delete or destroy? + # + # +has_many+ and +has_and_belongs_to_many+ associations have the methods <tt>destroy</tt>, + # <tt>delete</tt>, <tt>destroy_all</tt> and <tt>delete_all</tt>. + # + # For +has_and_belongs_to_many+, <tt>delete</tt> and <tt>destroy</tt> are the same: they + # cause the records in the join table to be removed. + # + # For +has_many+, <tt>destroy</tt> will always call the <tt>destroy</tt> method of the + # record(s) being removed so that callbacks are run. However <tt>delete</tt> will either + # do the deletion according to the strategy specified by the <tt>:dependent</tt> option, or + # if no <tt>:dependent</tt> option is given, then it will follow the default strategy. + # The default strategy is <tt>:nullify</tt> (set the foreign keys to <tt>nil</tt>), except for + # +has_many+ <tt>:through</tt>, where the default strategy is <tt>delete_all</tt> (delete + # the join records, without running their callbacks). + # + # There is also a <tt>clear</tt> method which is the same as <tt>delete_all</tt>, except that + # it returns the association rather than the records which have been deleted. + # + # === What gets deleted? + # + # There is a potential pitfall here: +has_and_belongs_to_many+ and +has_many+ <tt>:through</tt> + # associations have records in join tables, as well as the associated records. So when we + # call one of these deletion methods, what exactly should be deleted? + # + # The answer is that it is assumed that deletion on an association is about removing the + # <i>link</i> between the owner and the associated object(s), rather than necessarily the + # associated objects themselves. So with +has_and_belongs_to_many+ and +has_many+ + # <tt>:through</tt>, the join records will be deleted, but the associated records won't. + # + # This makes sense if you think about it: if you were to call <tt>post.tags.delete(Tag.find_by_name('food'))</tt> + # you would want the 'food' tag to be unlinked from the post, rather than for the tag itself + # to be removed from the database. + # + # However, there are examples where this strategy doesn't make sense. For example, suppose + # a person has many projects, and each project has many tasks. If we deleted one of a person's + # tasks, we would probably not want the project to be deleted. In this scenario, the delete method + # won't actually work: it can only be used if the association on the join model is a + # +belongs_to+. In other situations you are expected to perform operations directly on + # either the associated records or the <tt>:through</tt> association. + # + # With a regular +has_many+ there is no distinction between the "associated records" + # and the "link", so there is only one choice for what gets deleted. + # + # With +has_and_belongs_to_many+ and +has_many+ <tt>:through</tt>, if you want to delete the + # associated records themselves, you can always do something along the lines of + # <tt>person.tasks.each(&:destroy)</tt>. + # # == Type safety with <tt>ActiveRecord::AssociationTypeMismatch</tt> # # If you attempt to assign an object to an association that doesn't match the inferred @@ -828,6 +923,10 @@ module ActiveRecord # Removes one or more objects from the collection by setting their foreign keys to +NULL+. # Objects will be in addition destroyed if they're associated with <tt>:dependent => :destroy</tt>, # and deleted if they're associated with <tt>:dependent => :delete_all</tt>. + # + # If the <tt>:through</tt> option is used, then the join records are deleted (rather than + # nullified) by default, but you can specify <tt>:dependent => :destroy</tt> or + # <tt>:dependent => :nullify</tt> to override this. # [collection=objects] # Replaces the collections content by deleting and adding objects as appropriate. If the <tt>:through</tt> # option is true callbacks in the join models are triggered except destroy callbacks, since deletion is @@ -883,7 +982,7 @@ module ActiveRecord # * <tt>Firm#clients.create</tt> (similar to <tt>c = Client.new("firm_id" => id); c.save; c</tt>) # The declaration can also include an options hash to specialize the behavior of the association. # - # === Supported options + # === Options # [:class_name] # Specify the class name of the association. Use it only if that name can't be inferred # from the association name. So <tt>has_many :products</tt> will by default be linked @@ -911,7 +1010,9 @@ module ActiveRecord # objects' foreign keys are set to +NULL+ *without* calling their +save+ callbacks. If set to # <tt>:restrict</tt> this object cannot be deleted if it has any associated object. # - # *Warning:* This option is ignored when used with <tt>:through</tt> option. + # If using with the <tt>:through</tt> option, the association on the join model must be + # a +belongs_to+, and the records which get deleted are the join records, rather than + # the associated records. # # [:finder_sql] # Specify a complete SQL statement to fetch the association. This is a good way to go for complex @@ -991,12 +1092,7 @@ module ActiveRecord reflection = create_has_many_reflection(association_id, options, &extension) configure_dependency_for_has_many(reflection) add_association_callbacks(reflection.name, reflection.options) - - if options[:through] - collection_accessor_methods(reflection, HasManyThroughAssociation) - else - collection_accessor_methods(reflection, HasManyAssociation) - end + collection_accessor_methods(reflection) end # Specifies a one-to-one association with another class. This method should only be used @@ -1014,12 +1110,14 @@ module ActiveRecord # [build_association(attributes = {})] # Returns a new object of the associated type that has been instantiated # with +attributes+ and linked to this object through a foreign key, but has not - # yet been saved. <b>Note:</b> This ONLY works if an association already exists. - # It will NOT work if the association is +nil+. + # yet been saved. # [create_association(attributes = {})] # Returns a new object of the associated type that has been instantiated # with +attributes+, linked to this object through a foreign key, and that # has already been saved (if it passed the validation). + # [create_association!(attributes = {})] + # Does the same as <tt>create_association</tt>, but raises <tt>ActiveRecord::RecordInvalid</tt> + # if the record is invalid. # # (+association+ is replaced with the symbol passed as the first argument, so # <tt>has_one :manager</tt> would add among others <tt>manager.nil?</tt>.) @@ -1031,6 +1129,7 @@ module ActiveRecord # * <tt>Account#beneficiary=(beneficiary)</tt> (similar to <tt>beneficiary.account_id = account.id; beneficiary.save</tt>) # * <tt>Account#build_beneficiary</tt> (similar to <tt>Beneficiary.new("account_id" => id)</tt>) # * <tt>Account#create_beneficiary</tt> (similar to <tt>b = Beneficiary.new("account_id" => id); b.save; b</tt>) + # * <tt>Account#create_beneficiary!</tt> (similar to <tt>b = Beneficiary.new("account_id" => id); b.save!; b</tt>) # # === Options # @@ -1108,14 +1207,12 @@ module ActiveRecord def has_one(association_id, options = {}) if options[:through] reflection = create_has_one_through_reflection(association_id, options) - association_accessor_methods(reflection, ActiveRecord::Associations::HasOneThroughAssociation) else reflection = create_has_one_reflection(association_id, options) - association_accessor_methods(reflection, HasOneAssociation) - association_constructor_method(:build, reflection, HasOneAssociation) - association_constructor_method(:create, reflection, HasOneAssociation) + association_constructor_methods(reflection) configure_dependency_for_has_one(reflection) end + association_accessor_methods(reflection) end # Specifies a one-to-one association with another class. This method should only be used @@ -1137,6 +1234,9 @@ module ActiveRecord # Returns a new object of the associated type that has been instantiated # with +attributes+, linked to this object through a foreign key, and that # has already been saved (if it passed the validation). + # [create_association!(attributes = {})] + # Does the same as <tt>create_association</tt>, but raises <tt>ActiveRecord::RecordInvalid</tt> + # if the record is invalid. # # (+association+ is replaced with the symbol passed as the first argument, so # <tt>belongs_to :author</tt> would add among others <tt>author.nil?</tt>.) @@ -1148,6 +1248,7 @@ module ActiveRecord # * <tt>Post#author=(author)</tt> (similar to <tt>post.author_id = author.id</tt>) # * <tt>Post#build_author</tt> (similar to <tt>post.author = Author.new</tt>) # * <tt>Post#create_author</tt> (similar to <tt>post.author = Author.new; post.author.save; post.author</tt>) + # * <tt>Post#create_author!</tt> (similar to <tt>post.author = Author.new; post.author.save!; post.author</tt>) # The declaration can also include an options hash to specialize the behavior of the association. # # === Options @@ -1169,6 +1270,11 @@ module ActiveRecord # association will use "person_id" as the default <tt>:foreign_key</tt>. Similarly, # <tt>belongs_to :favorite_person, :class_name => "Person"</tt> will use a foreign key # of "favorite_person_id". + # [:foreign_type] + # Specify the column used to store the associated object's type, if this is a polymorphic + # association. By default this is guessed to be the name of the association with a "_type" + # suffix. So a class that defines a <tt>belongs_to :taggable, :polymorphic => true</tt> + # association will use "taggable_type" as the default <tt>:foreign_type</tt>. # [:primary_key] # Specify the method that returns the primary key of associated object used for the association. # By default this is id. @@ -1227,16 +1333,12 @@ module ActiveRecord def belongs_to(association_id, options = {}) reflection = create_belongs_to_reflection(association_id, options) - if reflection.options[:polymorphic] - association_accessor_methods(reflection, BelongsToPolymorphicAssociation) - association_foreign_type_setter_method(reflection) - else - association_accessor_methods(reflection, BelongsToAssociation) - association_constructor_method(:build, reflection, BelongsToAssociation) - association_constructor_method(:create, reflection, BelongsToAssociation) + association_accessor_methods(reflection) + + unless reflection.options[:polymorphic] + association_constructor_methods(reflection) end - association_foreign_key_setter_method(reflection) add_counter_cache_callbacks(reflection) if options[:counter_cache] add_touch_callbacks(reflection, options[:touch]) if options[:touch] @@ -1271,12 +1373,6 @@ module ActiveRecord # end # end # - # Deprecated: Any additional fields added to the join table will be placed as attributes when - # pulling records out through +has_and_belongs_to_many+ associations. Records returned from join - # tables with additional attributes will be marked as readonly (because we can't save changes - # to the additional attributes). It's strongly recommended that you upgrade any - # associations with attributes to a real join model (see introduction). - # # Adds the following methods for retrieval and query: # # [collection(force_reload = false)] @@ -1419,15 +1515,15 @@ module ActiveRecord # 'DELETE FROM developers_projects WHERE active=1 AND developer_id = #{id} AND project_id = #{record.id}' def has_and_belongs_to_many(association_id, options = {}, &extension) reflection = create_has_and_belongs_to_many_reflection(association_id, options, &extension) - collection_accessor_methods(reflection, HasAndBelongsToManyAssociation) + collection_accessor_methods(reflection) # Don't use a before_destroy callback since users' before_destroy # callbacks will be executed after the association is wiped out. include Module.new { class_eval <<-RUBY, __FILE__, __LINE__ + 1 def destroy # def destroy - #{reflection.name}.clear # posts.clear super # super + #{reflection.name}.clear # posts.clear end # end RUBY } @@ -1451,60 +1547,36 @@ module ActiveRecord table_name_prefix + join_table + table_name_suffix end - def association_accessor_methods(reflection, association_proxy_class) + def association_accessor_methods(reflection) redefine_method(reflection.name) do |*params| force_reload = params.first unless params.empty? - association = association_instance_get(reflection.name) - - if association.nil? || force_reload - association = association_proxy_class.new(self, reflection) - retval = force_reload ? reflection.klass.uncached { association.reload } : association.reload - if retval.nil? and association_proxy_class == BelongsToAssociation - association_instance_set(reflection.name, nil) - return nil - end - association_instance_set(reflection.name, association) - end - - association.target.nil? ? nil : association - end - - redefine_method("loaded_#{reflection.name}?") do - association = association_instance_get(reflection.name) - association && association.loaded? - end - - redefine_method("#{reflection.name}=") do |new_value| - association = association_instance_get(reflection.name) + association = association_proxy(reflection.name) - if association.nil? || association.target != new_value - association = association_proxy_class.new(self, reflection) + if force_reload + reflection.klass.uncached { association.reload } + elsif !association.loaded? || association.stale_target? + association.reload end - association.replace(new_value) - association_instance_set(reflection.name, new_value.nil? ? nil : association) + association.target.nil? ? nil : association end - redefine_method("set_#{reflection.name}_target") do |target| - return if target.nil? and association_proxy_class == BelongsToAssociation - association = association_proxy_class.new(self, reflection) - association.target = target - association_instance_set(reflection.name, association) + redefine_method("#{reflection.name}=") do |record| + association_proxy(reflection.name).replace(record) end end - def collection_reader_method(reflection, association_proxy_class) + def collection_reader_method(reflection) redefine_method(reflection.name) do |*params| force_reload = params.first unless params.empty? - association = association_instance_get(reflection.name) + association = association_proxy(reflection.name) - unless association - association = association_proxy_class.new(self, reflection) - association_instance_set(reflection.name, association) + if force_reload + reflection.klass.uncached { association.reload } + elsif association.stale_target? + association.reload end - reflection.klass.uncached { association.reload } if force_reload - association end @@ -1512,27 +1584,17 @@ module ActiveRecord if send(reflection.name).loaded? || reflection.options[:finder_sql] send(reflection.name).map { |r| r.id } else - if reflection.through_reflection && reflection.source_reflection.belongs_to? - through = reflection.through_reflection - primary_key = reflection.source_reflection.primary_key_name - send(through.name).select("DISTINCT #{through.quoted_table_name}.#{primary_key}").map! { |r| r.send(primary_key) } - else - send(reflection.name).select("#{reflection.quoted_table_name}.#{reflection.klass.primary_key}").except(:includes).map! { |r| r.id } - end + send(reflection.name).select("#{reflection.quoted_table_name}.#{reflection.klass.primary_key}").except(:includes).map! { |r| r.id } end end - end - def collection_accessor_methods(reflection, association_proxy_class, writer = true) - collection_reader_method(reflection, association_proxy_class) + def collection_accessor_methods(reflection, writer = true) + collection_reader_method(reflection) if writer redefine_method("#{reflection.name}=") do |new_value| - # Loads proxy class instance (defined in collection_reader_method) if not already loaded - association = send(reflection.name) - association.replace(new_value) - association + association_proxy(reflection.name).replace(new_value) end redefine_method("#{reflection.name.to_s.singularize}_ids=") do |new_value| @@ -1544,62 +1606,19 @@ module ActiveRecord end end - def association_constructor_method(constructor, reflection, association_proxy_class) - redefine_method("#{constructor}_#{reflection.name}") do |*params| - attributees = params.first unless params.empty? - replace_existing = params[1].nil? ? true : params[1] - association = association_instance_get(reflection.name) - - unless association - association = association_proxy_class.new(self, reflection) - association_instance_set(reflection.name, association) - end - - if association_proxy_class == HasOneAssociation - association.send(constructor, attributees, replace_existing) - else - association.send(constructor, attributees) - end - end - end - - def association_foreign_key_setter_method(reflection) - setters = reflect_on_all_associations(:belongs_to).map do |belongs_to_reflection| - if belongs_to_reflection.primary_key_name == reflection.primary_key_name - "association_instance_set(:#{belongs_to_reflection.name}, nil);" + def association_constructor_methods(reflection) + constructors = { + "build_#{reflection.name}" => "build", + "create_#{reflection.name}" => "create", + "create_#{reflection.name}!" => "create!" + } + + constructors.each do |name, proxy_name| + redefine_method(name) do |*params| + attributes = params.first unless params.empty? + association_proxy(reflection.name).send(proxy_name, attributes) end - end.compact.join - - if method_defined?(:"#{reflection.primary_key_name}=") - undef_method :"#{reflection.primary_key_name}=" end - - class_eval <<-FILE, __FILE__, __LINE__ + 1 - def #{reflection.primary_key_name}=(new_id) - write_attribute :#{reflection.primary_key_name}, new_id - if #{reflection.primary_key_name}_changed? - #{ setters } - end - end - FILE - end - - def association_foreign_type_setter_method(reflection) - setters = reflect_on_all_associations(:belongs_to).map do |belongs_to_reflection| - if belongs_to_reflection.options[:foreign_type] == reflection.options[:foreign_type] - "association_instance_set(:#{belongs_to_reflection.name}, nil);" - end - end.compact.join - - field = reflection.options[:foreign_type] - class_eval <<-FILE, __FILE__, __LINE__ + 1 - def #{field}=(new_id) - write_attribute :#{field}, new_id - if #{field}_changed? - #{ setters } - end - end - FILE end def add_counter_cache_callbacks(reflection) @@ -1648,56 +1667,39 @@ module ActiveRecord # - set the foreign key to NULL if the option is set to :nullify # - do not delete the parent record if there is any child record if the # option is set to :restrict - # - # The +extra_conditions+ parameter, which is not used within the main - # Active Record codebase, is meant to allow plugins to define extra - # finder conditions. - def configure_dependency_for_has_many(reflection, extra_conditions = nil) - if reflection.options.include?(:dependent) + def configure_dependency_for_has_many(reflection) + if reflection.options[:dependent] + method_name = "has_many_dependent_for_#{reflection.name}" + case reflection.options[:dependent] - when :destroy - method_name = "has_many_dependent_destroy_for_#{reflection.name}".to_sym - define_method(method_name) do + when :destroy, :delete_all, :nullify + define_method(method_name) do + if reflection.options[:dependent] == :destroy send(reflection.name).each do |o| # No point in executing the counter update since we're going to destroy the parent anyway counter_method = ('belongs_to_counter_cache_before_destroy_for_' + self.class.name.downcase).to_sym - if(o.respond_to? counter_method) then + if o.respond_to?(counter_method) class << o self end.send(:define_method, counter_method, Proc.new {}) end - o.destroy end end - before_destroy method_name - when :delete_all - before_destroy do |record| - self.class.send(:delete_all_has_many_dependencies, - record, - reflection.name, - reflection.klass, - reflection.dependent_conditions(record, self.class, extra_conditions)) - end - when :nullify - before_destroy do |record| - self.class.send(:nullify_has_many_dependencies, - record, - reflection.name, - reflection.klass, - reflection.primary_key_name, - reflection.dependent_conditions(record, self.class, extra_conditions)) - end - when :restrict - method_name = "has_many_dependent_restrict_for_#{reflection.name}".to_sym - define_method(method_name) do - unless send(reflection.name).empty? - raise DeleteRestrictionError.new(reflection) - end + + # AssociationProxy#delete_all looks at the :dependent option and acts accordingly + send(reflection.name).delete_all + end + when :restrict + define_method(method_name) do + unless send(reflection.name).empty? + raise DeleteRestrictionError.new(reflection) end - before_destroy method_name - else - raise ArgumentError, "The :dependent option expects either :destroy, :delete_all, :nullify or :restrict (#{reflection.options[:dependent].inspect})" + end + else + raise ArgumentError, "The :dependent option expects either :destroy, :delete_all, :nullify or :restrict (#{reflection.options[:dependent].inspect})" end + + before_destroy method_name end end @@ -1722,7 +1724,7 @@ module ActiveRecord class_eval <<-eoruby, __FILE__, __LINE__ + 1 def #{method_name} association = #{reflection.name} - association.update_attribute(#{reflection.primary_key_name.inspect}, nil) if association + association.update_attribute(#{reflection.foreign_key.inspect}, nil) if association end eoruby when :restrict @@ -1760,14 +1762,6 @@ module ActiveRecord end end - def delete_all_has_many_dependencies(record, reflection_name, association_class, dependent_conditions) - association_class.delete_all(dependent_conditions) - end - - def nullify_has_many_dependencies(record, reflection_name, association_class, primary_key_name, dependent_conditions) - association_class.update_all("#{primary_key_name} = NULL", dependent_conditions) - end - mattr_accessor :valid_keys_for_has_many_association @@valid_keys_for_has_many_association = [ :class_name, :table_name, :foreign_key, :primary_key, @@ -1816,13 +1810,7 @@ module ActiveRecord def create_belongs_to_reflection(association_id, options) options.assert_valid_keys(valid_keys_for_belongs_to_association) - reflection = create_reflection(:belongs_to, association_id, options, self) - - if options[:polymorphic] - reflection.options[:foreign_type] ||= reflection.class_name.underscore + "_type" - end - - reflection + create_reflection(:belongs_to, association_id, options, self) end mattr_accessor :valid_keys_for_has_and_belongs_to_many_association @@ -1842,7 +1830,7 @@ module ActiveRecord reflection = create_reflection(:has_and_belongs_to_many, association_id, options, self) - if reflection.association_foreign_key == reflection.primary_key_name + if reflection.association_foreign_key == reflection.foreign_key raise HasAndBelongsToManyAssociationForeignKeyNeeded.new(reflection) end diff --git a/activerecord/lib/active_record/associations/association_collection.rb b/activerecord/lib/active_record/associations/association_collection.rb index 11a7a725e5..ca350f51c9 100644 --- a/activerecord/lib/active_record/associations/association_collection.rb +++ b/activerecord/lib/active_record/associations/association_collection.rb @@ -1,4 +1,3 @@ -require 'set' require 'active_support/core_ext/array/wrap' module ActiveRecord @@ -23,98 +22,55 @@ module ActiveRecord def select(select = nil) if block_given? - load_target - @target.select.each { |e| yield e } + load_target.select.each { |e| yield e } else scoped.select(select) end end - def scoped - with_scope(@scope) { @reflection.klass.scoped } - end - def find(*args) - options = args.extract_options! - - # If using a custom finder_sql, scan the entire collection. if @reflection.options[:finder_sql] - expects_array = args.first.kind_of?(Array) - ids = args.flatten.compact.uniq.map { |arg| arg.to_i } - - if ids.size == 1 - id = ids.first - record = load_target.detect { |r| id == r.id } - expects_array ? [ record ] : record - else - load_target.select { |r| ids.include?(r.id) } - end + find_by_scan(*args) else - merge_options_from_reflection!(options) - construct_find_options!(options) - - with_scope(:find => @scope[:find].slice(:conditions, :order)) do - relation = @reflection.klass.send(:construct_finder_arel, options, @reflection.klass.send(:current_scoped_methods)) - - case args.first - when :first, :last - relation.send(args.first) - when :all - records = relation.all - @reflection.options[:uniq] ? uniq(records) : records - else - relation.find(*args) - end - end + scoped.find(*args) end end - # Fetches the first one using SQL if possible. def first(*args) - if fetch_first_or_last_using_find?(args) - find(:first, *args) - else - load_target unless loaded? - args = args[1..-1] if args.first.kind_of?(Hash) && args.first.empty? - @target.first(*args) - end + first_or_last(:first, *args) end - # Fetches the last one using SQL if possible. def last(*args) - if fetch_first_or_last_using_find?(args) - find(:last, *args) - else - load_target unless loaded? - @target.last(*args) - end + first_or_last(:last, *args) end def to_ary - load_target - if @target.is_a?(Array) - @target.to_ary - else - Array.wrap(@target) - end + load_target.dup end alias_method :to_a, :to_ary def reset - reset_target! - reset_scopes_cache! - @loaded = false + @_scopes_cache = {} + @loaded = false + @target = [] end def build(attributes = {}, &block) - if attributes.is_a?(Array) - attributes.collect { |attr| build(attr, &block) } - else - build_record(attributes) do |record| - block.call(record) if block_given? - set_belongs_to_association_for(record) - end + build_or_create(attributes, :build, &block) + end + + def create(attributes = {}, &block) + unless @owner.persisted? + raise ActiveRecord::RecordNotSaved, "You cannot call create unless the parent is saved" end + + build_or_create(attributes, :create, &block) + end + + def create!(attrs = {}, &block) + record = create(attrs, &block) + Array.wrap(record).each(&:save!) + record end # Add +records+ to this association. Returns +self+ so method calls may be chained. @@ -124,9 +80,9 @@ module ActiveRecord load_target if @owner.new_record? transaction do - flatten_deeper(records).each do |record| + records.flatten.each do |record| raise_on_type_mismatch(record) - add_record_to_target_with_callbacks(record) do |r| + add_to_target(record) do |r| result &&= insert_record(record) unless @owner.new_record? end end @@ -157,18 +113,35 @@ module ActiveRecord # # See delete for more info. def delete_all - load_target - delete(@target) - reset_target! - reset_scopes_cache! + delete(load_target).tap do + reset + loaded! + end + end + + # Identical to delete_all, except that the return value is the association (for chaining) + # rather than the records which have been removed. + def clear + delete_all + self + end + + # Destroy all the records from this association. + # + # See destroy for more info. + def destroy_all + destroy(load_target).tap do + reset + loaded! + end end # Calculate sum using SQL, not Enumerable def sum(*args) if block_given? - calculate(:sum, *args) { |*block_args| yield(*block_args) } + scoped.sum(*args) { |*block_args| yield(*block_args) } else - calculate(:sum, *args) + scoped.sum(*args) end end @@ -185,14 +158,13 @@ module ActiveRecord @reflection.klass.count_by_sql(custom_counter_sql) else - if @reflection.options[:uniq] # This is needed because 'SELECT count(DISTINCT *)..' is not valid SQL. - column_name = "#{@reflection.quoted_table_name}.#{@reflection.klass.primary_key}" unless column_name + column_name ||= @reflection.klass.primary_key options.merge!(:distinct => true) end - value = @reflection.klass.send(:with_scope, @scope) { @reflection.klass.count(column_name, options) } + value = scoped.count(column_name, options) limit = @reflection.options[:limit] offset = @reflection.options[:offset] @@ -213,10 +185,7 @@ module ActiveRecord # are actually removed from the database, that depends precisely on # +delete_records+. They are in any case removed from the collection. def delete(*records) - remove_records(records) do |_records, old_records| - delete_records(old_records) if old_records.any? - _records.each { |record| @target.delete(record) } - end + delete_or_destroy(records, @reflection.options[:dependent]) end # Destroy +records+ and remove them from this association calling @@ -225,54 +194,8 @@ module ActiveRecord # Note that this method will _always_ remove records from the database # ignoring the +:dependent+ option. def destroy(*records) - records = find(records) if records.any? {|record| record.kind_of?(Fixnum) || record.kind_of?(String)} - remove_records(records) do |_records, old_records| - old_records.each { |record| record.destroy } - end - - load_target - end - - # Removes all records from this association. Returns +self+ so method calls may be chained. - def clear - unless length.zero? # forces load_target if it hasn't happened already - if @reflection.options[:dependent] == :destroy - destroy_all - else - delete_all - end - end - - self - end - - # Destroy all the records from this association. - # - # See destroy for more info. - def destroy_all - load_target - destroy(@target).tap do - reset_target! - reset_scopes_cache! - end - end - - def create(attrs = {}) - if attrs.is_a?(Array) - attrs.collect { |attr| create(attr) } - else - create_record(attrs) do |record| - yield(record) if block_given? - record.save - end - end - end - - def create!(attrs = {}) - create_record(attrs) do |record| - yield(record) if block_given? - record.save! - end + records = find(records) if records.any? { |record| record.kind_of?(Fixnum) || record.kind_of?(String) } + delete_or_destroy(records, :destroy) end # Returns the size of the collection by executing a SELECT COUNT(*) @@ -316,7 +239,7 @@ module ActiveRecord def any? if block_given? - method_missing(:any?) { |*block_args| yield(*block_args) } + load_target.any? { |*block_args| yield(*block_args) } else !empty? end @@ -325,7 +248,7 @@ module ActiveRecord # Returns true if the collection has more than 1 record. Equivalent to collection.size > 1. def many? if block_given? - method_missing(:many?) { |*block_args| yield(*block_args) } + load_target.many? { |*block_args| yield(*block_args) } else size > 1 end @@ -342,108 +265,117 @@ module ActiveRecord # This will perform a diff and delete/add only records that have changed. def replace(other_array) other_array.each { |val| raise_on_type_mismatch(val) } - - load_target - other = other_array.size < 100 ? other_array : other_array.to_set - current = @target.size < 100 ? @target : @target.to_set + original_target = load_target.dup transaction do - delete(@target.select { |v| !other.include?(v) }) - concat(other_array.select { |v| !current.include?(v) }) + delete(@target - other_array) + + unless concat(other_array - @target) + @target = original_target + raise RecordNotSaved, "Failed to replace #{@reflection.name} because one or more of the " \ + "new records could not be saved." + end end end def include?(record) - return false unless record.is_a?(@reflection.klass) - return include_in_memory?(record) if record.new_record? - load_target if @reflection.options[:finder_sql] && !loaded? - loaded? ? @target.include?(record) : exists?(record) + if record.is_a?(@reflection.klass) + if record.new_record? + include_in_memory?(record) + else + load_target if @reflection.options[:finder_sql] + loaded? ? @target.include?(record) : scoped.exists?(record) + end + else + false + end end - def proxy_respond_to?(method, include_private = false) + def respond_to?(method, include_private = false) super || @reflection.klass.respond_to?(method, include_private) end + def method_missing(method, *args, &block) + match = DynamicFinderMatch.match(method) + if match && match.creator? + attributes = match.attribute_names + return send(:"find_by_#{attributes.join('_and_')}", *args) || create(Hash[attributes.zip(args)]) + end + + if @target.respond_to?(method) || (!@reflection.klass.respond_to?(method) && Class.respond_to?(method)) + super + elsif @reflection.klass.scopes[method] + @_scopes_cache ||= {} + @_scopes_cache[method] ||= {} + @_scopes_cache[method][args] ||= scoped.readonly(nil).send(method, *args) + else + scoped.readonly(nil).send(method, *args, &block) + end + end + protected - def construct_find_options!(options) + + def association_scope + options = @reflection.options.slice(:order, :limit, :joins, :group, :having, :offset) + super.apply_finder_options(options) end def load_target - if !@owner.new_record? || foreign_key_present + if find_target? + targets = [] + begin - unless loaded? - if @target.is_a?(Array) && @target.any? - @target = find_target.map do |f| - i = @target.index(f) - if i - @target.delete_at(i).tap do |t| - keys = ["id"] + t.changes.keys + (f.attribute_names - t.attribute_names) - f.attributes.except(*keys).each do |k,v| - t.send("#{k}=", v) - end - end - else - f - end - end + @target - else - @target = find_target - end - end + targets = find_target rescue ActiveRecord::RecordNotFound reset end + + @target = merge_target_lists(targets, @target) end - loaded if target + loaded! target end - def method_missing(method, *args) - match = DynamicFinderMatch.match(method) - if match && match.creator? - attributes = match.attribute_names - return send(:"find_by_#{attributes.join('_and_')}", *args) || create(Hash[attributes.zip(args)]) - end + def add_to_target(record) + transaction do + callback(:before_add, record) + yield(record) if block_given? - if @target.respond_to?(method) || (!@reflection.klass.respond_to?(method) && Class.respond_to?(method)) - super - elsif @reflection.klass.scopes[method] - @_scopes_cache ||= {} - @_scopes_cache[method] ||= {} - @_scopes_cache[method][args] ||= with_scope(@scope) { @reflection.klass.send(method, *args) } - else - with_scope(@scope) do - if block_given? - @reflection.klass.send(method, *args) { |*block_args| yield(*block_args) } - else - @reflection.klass.send(method, *args) - end + if @reflection.options[:uniq] && index = @target.index(record) + @target[index] = record + else + @target << record end + + callback(:after_add, record) + set_inverse_instance(record) end + + record + end + + private + + def select_value + super || uniq_select_value + end + + def uniq_select_value + @reflection.options[:uniq] && "DISTINCT #{@reflection.quoted_table_name}.*" end def custom_counter_sql if @reflection.options[:counter_sql] - counter_sql = @reflection.options[:counter_sql] + interpolate(@reflection.options[:counter_sql]) else # replace the SELECT clause with COUNT(*), preserving any hints within /* ... */ - counter_sql = @reflection.options[:finder_sql].sub(/SELECT\b(\/\*.*?\*\/ )?(.*)\bFROM\b/im) { "SELECT #{$1}COUNT(*) FROM" } + interpolate(@reflection.options[:finder_sql]).sub(/SELECT\b(\/\*.*?\*\/ )?(.*)\bFROM\b/im) { "SELECT #{$1}COUNT(*) FROM" } end - - interpolate_sql(counter_sql) end def custom_finder_sql - interpolate_sql(@reflection.options[:finder_sql]) - end - - def reset_target! - @target = Array.new - end - - def reset_scopes_cache! - @_scopes_cache = {} + interpolate(@reflection.options[:finder_sql]) end def find_target @@ -455,65 +387,74 @@ module ActiveRecord end records = @reflection.options[:uniq] ? uniq(records) : records - records.each do |record| - set_inverse_instance(record, @owner) - end + records.each { |record| set_inverse_instance(record) } records end - def add_record_to_target_with_callbacks(record) - callback(:before_add, record) - yield(record) if block_given? - @target ||= [] unless loaded? - if index = @target.index(record) - @target[index] = record - else - @target << record - end - callback(:after_add, record) - set_inverse_instance(record, @owner) - record + def merge_target_lists(loaded, existing) + return loaded if existing.empty? + return existing if loaded.empty? + + loaded.map do |f| + i = existing.index(f) + if i + existing.delete_at(i).tap do |t| + keys = ["id"] + t.changes.keys + (f.attribute_names - t.attribute_names) + # FIXME: this call to attributes causes many NoMethodErrors + attributes = f.attributes + (attributes.keys - keys).each do |k| + t.send("#{k}=", attributes[k]) + end + end + else + f + end + end + existing end - private - def create_record(attrs) - attrs.update(@reflection.options[:conditions]) if @reflection.options[:conditions].is_a?(Hash) - ensure_owner_is_persisted! - - scoped_where = scoped.where_values_hash - create_scope = scoped_where ? @scope[:create].merge(scoped_where) : @scope[:create] - record = @reflection.klass.send(:with_scope, :create => create_scope) do - @reflection.build_association(attrs) - end - if block_given? - add_record_to_target_with_callbacks(record) { |*block_args| yield(*block_args) } - else - add_record_to_target_with_callbacks(record) + def build_or_create(attributes, method) + records = Array.wrap(attributes).map do |attrs| + record = build_record(attrs) + + add_to_target(record) do + yield(record) if block_given? + insert_record(record) if method == :create + end end + + attributes.is_a?(Array) ? records : records.first end - def build_record(attrs) - attrs.update(@reflection.options[:conditions]) if @reflection.options[:conditions].is_a?(Hash) - record = @reflection.build_association(attrs) - if block_given? - add_record_to_target_with_callbacks(record) { |*block_args| yield(*block_args) } - else - add_record_to_target_with_callbacks(record) - end + # Do the relevant stuff to insert the given record into the association collection. + def insert_record(record, validate = true) + raise NotImplementedError end - def remove_records(*records) - records = flatten_deeper(records) + def build_record(attributes) + @reflection.build_association(scoped.scope_for_create.merge(attributes)) + end + + def delete_or_destroy(records, method) + records = records.flatten records.each { |record| raise_on_type_mismatch(record) } + existing_records = records.reject { |r| r.new_record? } transaction do records.each { |record| callback(:before_remove, record) } - old_records = records.reject { |r| r.new_record? } - yield(records, old_records) + + delete_records(existing_records, method) if existing_records.any? + records.each { |record| @target.delete(record) } + records.each { |record| callback(:after_remove, record) } end end + # Delete the given records from the association, using one of the methods :destroy, + # :delete_all or :nullify (or nil, in which case a default is used). + def delete_records(records, method) + raise NotImplementedError + end + def callback(method, record) callbacks_for(method).each do |callback| case callback @@ -532,27 +473,61 @@ module ActiveRecord @owner.class.send(full_callback_name.to_sym) || [] end - def ensure_owner_is_persisted! - unless @owner.persisted? - raise ActiveRecord::RecordNotSaved, "You cannot call create unless the parent is saved" - end - end - + # Should we deal with assoc.first or assoc.last by issuing an independent query to + # the database, or by getting the target, and then taking the first/last item from that? + # + # If the args is just a non-empty options hash, go to the database. + # + # Otherwise, go to the database only if none of the following are true: + # * target already loaded + # * owner is new record + # * custom :finder_sql exists + # * target contains new or changed record(s) + # * the first arg is an integer (which indicates the number of records to be returned) def fetch_first_or_last_using_find?(args) - (args.first.kind_of?(Hash) && !args.first.empty?) || !(loaded? || @owner.new_record? || @reflection.options[:finder_sql] || - @target.any? { |record| record.new_record? } || args.first.kind_of?(Integer)) + if args.first.is_a?(Hash) + true + else + !(loaded? || + @owner.new_record? || + @reflection.options[:finder_sql] || + @target.any? { |record| record.new_record? || record.changed? } || + args.first.kind_of?(Integer)) + end end def include_in_memory?(record) if @reflection.is_a?(ActiveRecord::Reflection::ThroughReflection) - @owner.send(proxy_reflection.through_reflection.name.to_sym).any? do |source| + @owner.send(proxy_reflection.through_reflection.name).any? { |source| target = source.send(proxy_reflection.source_reflection.name) target.respond_to?(:include?) ? target.include?(record) : target == record - end + } || @target.include?(record) else @target.include?(record) end end + + # If using a custom finder_sql, #find scans the entire collection. + def find_by_scan(*args) + expects_array = args.first.kind_of?(Array) + ids = args.flatten.compact.uniq.map { |arg| arg.to_i } + + if ids.size == 1 + id = ids.first + record = load_target.detect { |r| id == r.id } + expects_array ? [ record ] : record + else + load_target.select { |r| ids.include?(r.id) } + end + end + + # Fetches the first/last using SQL if possible, otherwise from the target array. + def first_or_last(type, *args) + args.shift if args.first.is_a?(Hash) && args.first.empty? + + collection = fetch_first_or_last_using_find?(args) ? scoped : load_target + collection.send(type, *args) + end end end end diff --git a/activerecord/lib/active_record/associations/association_proxy.rb b/activerecord/lib/active_record/associations/association_proxy.rb index 53ec5a0da6..d16cda5585 100644 --- a/activerecord/lib/active_record/associations/association_proxy.rb +++ b/activerecord/lib/active_record/associations/association_proxy.rb @@ -4,17 +4,18 @@ module ActiveRecord module Associations # = Active Record Associations # - # This is the root class of all association proxies: + # This is the root class of all association proxies ('+ Foo' signifies an included module Foo): # # AssociationProxy - # BelongsToAssociation + # SingularAssociaton # HasOneAssociation - # BelongsToPolymorphicAssociation + # HasOneThroughAssociation + ThroughAssociation + # BelongsToAssociation + # BelongsToPolymorphicAssociation # AssociationCollection # HasAndBelongsToManyAssociation # HasManyAssociation - # HasManyThroughAssociation - # HasOneThroughAssociation + # HasManyThroughAssociation + ThroughAssociation # # Association proxies in Active Record are middlemen between the object that # holds the association, known as the <tt>@owner</tt>, and the actual associated @@ -50,10 +51,9 @@ module ActiveRecord # is computed directly through SQL and does not trigger by itself the # instantiation of the actual post records. class AssociationProxy #:nodoc: - alias_method :proxy_respond_to?, :respond_to? alias_method :proxy_extend, :extend - delegate :to_param, :to => :proxy_target - instance_methods.each { |m| undef_method m unless m.to_s =~ /^(?:nil\?|send|object_id|to_a)$|^__|^respond_to_missing|proxy_/ } + + instance_methods.each { |m| undef_method m unless m.to_s =~ /^(?:nil\?|send|object_id|to_a)$|^__|^respond_to|proxy_/ } def initialize(owner, reflection) @owner, @reflection = owner, reflection @@ -64,6 +64,10 @@ module ActiveRecord construct_scope end + def to_param + proxy_target.to_param + end + # Returns the owner of the proxy. def proxy_owner @owner @@ -75,21 +79,25 @@ module ActiveRecord @reflection end - # Returns the \target of the proxy, same as +target+. - def proxy_target - @target - end - # Does the proxy or its \target respond to +symbol+? def respond_to?(*args) - proxy_respond_to?(*args) || (load_target && @target.respond_to?(*args)) + super || (load_target && @target.respond_to?(*args)) + end + + # Forwards any missing method call to the \target. + def method_missing(method, *args, &block) + if load_target + return super unless @target.respond_to?(method) + @target.send(method, *args, &block) + end + rescue NoMethodError => e + raise e, e.message.sub(/ for #<.*$/, " via proxy for #{@target}") end # Forwards <tt>===</tt> explicitly to the \target because the instance method # removal above doesn't catch it. Loads the \target if needed. def ===(other) - load_target - other === @target + other === load_target end # Returns the name of the table of the related class: @@ -100,13 +108,6 @@ module ActiveRecord @reflection.klass.table_name end - # Returns the SQL string that corresponds to the <tt>:conditions</tt> - # option of the macro, if given, or +nil+ otherwise. - def conditions - @conditions ||= interpolate_sql(@reflection.sanitized_conditions) if @reflection.sanitized_conditions - end - alias :sql_conditions :conditions - # Resets the \loaded flag to +false+ and sets the \target to +nil+. def reset @loaded = false @@ -117,6 +118,7 @@ module ActiveRecord # Reloads the \target and returns +self+ on success. def reload reset + construct_scope load_target self unless @target.nil? end @@ -127,117 +129,93 @@ module ActiveRecord end # Asserts the \target has been loaded setting the \loaded flag to +true+. - def loaded - @loaded = true + def loaded! + @loaded = true + @stale_state = stale_state end - # Returns the target of this proxy, same as +proxy_target+. - def target - @target + # The target is stale if the target no longer points to the record(s) that the + # relevant foreign_key(s) refers to. If stale, the association accessor method + # on the owner will reload the target. It's up to subclasses to implement the + # state_state method if relevant. + # + # Note that if the target has not been loaded, it is not considered stale. + def stale_target? + loaded? && @stale_state != stale_state end + # Returns the target of this proxy, same as +proxy_target+. + attr_reader :target + + # Returns the \target of the proxy, same as +target+. + alias :proxy_target :target + # Sets the target of this proxy to <tt>\target</tt>, and the \loaded flag to +true+. def target=(target) @target = target - loaded + loaded! end # Forwards the call to the target. Loads the \target if needed. def inspect - load_target - @target.inspect + load_target.inspect end def send(method, *args) - if proxy_respond_to?(method) - super - else - load_target - @target.send(method, *args) - end + return super if respond_to?(method) + load_target.send(method, *args) end - protected - # Does the association have a <tt>:dependent</tt> option? - def dependent? - @reflection.options[:dependent] - end + def scoped + target_scope.merge(@association_scope) + end - def interpolate_sql(sql, record = nil) - @owner.send(:interpolate_sql, sql, record) - end + protected - # Forwards the call to the reflection class. - def sanitize_sql(sql, table_name = @reflection.klass.table_name) - @reflection.klass.send(:sanitize_sql, sql, table_name) + # Construct the scope for this association. + # + # Note that the association_scope is merged into the targed_scope only when the + # scoped method is called. This is because at that point the call may be surrounded + # by scope.scoping { ... } or with_scope { ... } etc, which affects the scope which + # actually gets built. + def construct_scope + @association_scope = association_scope if target_klass end - # Assigns the ID of the owner to the corresponding foreign key in +record+. - # If the association is polymorphic the type of the owner is also set. - def set_belongs_to_association_for(record) - if @reflection.options[:as] - record["#{@reflection.options[:as]}_id"] = @owner.id if @owner.persisted? - record["#{@reflection.options[:as]}_type"] = @owner.class.base_class.name.to_s - else - if @owner.persisted? - primary_key = @reflection.options[:primary_key] || :id - record[@reflection.primary_key_name] = @owner.send(primary_key) - end + def association_scope + scope = target_klass.unscoped + scope = scope.create_with(creation_attributes) + scope = scope.apply_finder_options(@reflection.options.slice(:readonly, :include)) + scope = scope.where(interpolate(@reflection.options[:conditions])) + if select = select_value + scope = scope.select(select) end + scope = scope.extending(*Array.wrap(@reflection.options[:extend])) + scope.where(construct_owner_conditions) end - # Merges into +options+ the ones coming from the reflection. - def merge_options_from_reflection!(options) - options.reverse_merge!( - :group => @reflection.options[:group], - :having => @reflection.options[:having], - :limit => @reflection.options[:limit], - :offset => @reflection.options[:offset], - :joins => @reflection.options[:joins], - :include => @reflection.options[:include], - :select => @reflection.options[:select], - :readonly => @reflection.options[:readonly] - ) + def aliased_table + target_klass.arel_table end - # Forwards +with_scope+ to the reflection. - def with_scope(*args, &block) - @reflection.klass.send :with_scope, *args, &block - end - - # Construct the scope used for find/create queries on the target - def construct_scope - @scope = { - :find => construct_find_scope, - :create => construct_create_scope - } + # Set the inverse association, if possible + def set_inverse_instance(record) + if record && invertible_for?(record) + inverse = record.send(:association_proxy, inverse_reflection_for(record).name) + inverse.target = @owner + end end - # Implemented by subclasses - def construct_find_scope - raise NotImplementedError + # This class of the target. belongs_to polymorphic overrides this to look at the + # polymorphic_type field on the owner. + def target_klass + @reflection.klass end - # Implemented by (some) subclasses - def construct_create_scope - {} - end - - private - # Forwards any missing method call to the \target. - def method_missing(method, *args) - if load_target - unless @target.respond_to?(method) - message = "undefined method `#{method.to_s}' for \"#{@target}\":#{@target.class.to_s}" - raise NoMethodError, message - end - - if block_given? - @target.send(method, *args) { |*block_args| yield(*block_args) } - else - @target.send(method, *args) - end - end + # Can be overridden (i.e. in ThroughAssociation) to merge in other scopes (i.e. the + # through association's scope) + def target_scope + target_klass.scoped end # Loads the \target if needed and returns it. @@ -251,26 +229,84 @@ module ActiveRecord # ActiveRecord::RecordNotFound is rescued within the method, and it is # not reraised. The proxy is \reset and +nil+ is the return value. def load_target - return nil unless defined?(@loaded) - - if !loaded? && (!@owner.new_record? || foreign_key_present) - if IdentityMap.enabled? && association_class - @target = IdentityMap.get(association_class, @owner[@reflection.association_foreign_key]) + if find_target? + begin + if IdentityMap.enabled? && association_class && association_class.respond_to?(:base_class) + @target = IdentityMap.get(association_class, @owner[@reflection.foreign_key]) + end + rescue NameError + nil + ensure + @target ||= find_target end - @target ||= find_target end - - @loaded = true - @target + loaded! + target rescue ActiveRecord::RecordNotFound reset end - # Can be overwritten by associations that might have the foreign key - # available for an association without having the object itself (and - # still being a new record). Currently, only +belongs_to+ presents - # this scenario (both vanilla and polymorphic). - def foreign_key_present + private + + def find_target? + !loaded? && (!@owner.new_record? || foreign_key_present?) && target_klass + end + + def interpolate(sql, record = nil) + if sql.respond_to?(:to_proc) + @owner.send(:instance_exec, record, &sql) + else + sql + end + end + + def select_value + @reflection.options[:select] + end + + # Implemented by (some) subclasses + def creation_attributes + { } + end + + # Returns a hash linking the owner to the association represented by the reflection + def construct_owner_attributes(reflection = @reflection) + attributes = {} + if reflection.macro == :belongs_to + attributes[reflection.association_primary_key] = @owner[reflection.foreign_key] + else + attributes[reflection.foreign_key] = @owner[reflection.active_record_primary_key] + + if reflection.options[:as] + attributes["#{reflection.options[:as]}_type"] = @owner.class.base_class.name + end + end + attributes + end + + # Builds an array of arel nodes from the owner attributes hash + def construct_owner_conditions(table = aliased_table, reflection = @reflection) + conditions = construct_owner_attributes(reflection).map do |attr, value| + table[attr].eq(value) + end + table.create_and(conditions) + end + + # Sets the owner attributes on the given record + def set_owner_attributes(record) + if @owner.persisted? + construct_owner_attributes.each { |key, value| record[key] = value } + end + end + + # Should be true if there is a foreign key present on the @owner which + # references the target. This is used to determine whether we can load + # the target if the @owner is currently a new record (and therefore + # without a key). + # + # Currently implemented by belongs_to (vanilla and polymorphic) and + # has_one/has_many :through associations which go through a belongs_to + def foreign_key_present? false end @@ -284,34 +320,24 @@ module ActiveRecord end end - if RUBY_VERSION < '1.9.2' - # Array#flatten has problems with recursive arrays before Ruby 1.9.2. - # Going one level deeper solves the majority of the problems. - def flatten_deeper(array) - array.collect { |element| (element.respond_to?(:flatten) && !element.is_a?(Hash)) ? element.flatten : element }.flatten - end - else - def flatten_deeper(array) - array.flatten - end + # Can be redefined by subclasses, notably polymorphic belongs_to + # The record parameter is necessary to support polymorphic inverses as we must check for + # the association in the specific class of the record. + def inverse_reflection_for(record) + @reflection.inverse_of end - # Returns the ID of the owner, quoted if needed. - def owner_quoted_id - @owner.quoted_id + # Is this association invertible? Can be redefined by subclasses. + def invertible_for?(record) + inverse_reflection_for(record) end - def set_inverse_instance(record, instance) - return if record.nil? || !we_can_set_the_inverse_on_this?(record) - inverse_relationship = @reflection.inverse_of - unless inverse_relationship.nil? - record.send(:"set_#{inverse_relationship.name}_target", instance) - end - end - - # Override in subclasses - def we_can_set_the_inverse_on_this?(record) - false + # This should be implemented to return the values of the relevant key(s) on the owner, + # so that when state_state is different from the value stored on the last find_target, + # the target is stale. + # + # This is only relevant to certain associations, which is why it returns nil by default. + def stale_state end def association_class diff --git a/activerecord/lib/active_record/associations/belongs_to_association.rb b/activerecord/lib/active_record/associations/belongs_to_association.rb index b438620c8f..178c7204f8 100644 --- a/activerecord/lib/active_record/associations/belongs_to_association.rb +++ b/activerecord/lib/active_record/associations/belongs_to_association.rb @@ -1,41 +1,17 @@ module ActiveRecord # = Active Record Belongs To Associations module Associations - class BelongsToAssociation < AssociationProxy #:nodoc: - def create(attributes = {}) - replace(@reflection.create_association(attributes)) - end - - def build(attributes = {}) - replace(@reflection.build_association(attributes)) - end - + class BelongsToAssociation < SingularAssociation #:nodoc: def replace(record) - counter_cache_name = @reflection.counter_cache_column - - if record.nil? - if counter_cache_name && @owner.persisted? - @reflection.klass.decrement_counter(counter_cache_name, previous_record_id) if @owner[@reflection.primary_key_name] - end - - @target = @owner[@reflection.primary_key_name] = nil - else - raise_on_type_mismatch(record) + record = check_record(record) - if counter_cache_name && @owner.persisted? && record.id != @owner[@reflection.primary_key_name] - @reflection.klass.increment_counter(counter_cache_name, record.id) - @reflection.klass.decrement_counter(counter_cache_name, @owner[@reflection.primary_key_name]) if @owner[@reflection.primary_key_name] - end - - @target = (AssociationProxy === record ? record.target : record) - @owner[@reflection.primary_key_name] = record_id(record) if record.persisted? - @updated = true - end + update_counters(record) + replace_keys(record) + set_inverse_instance(record) - set_inverse_instance(record, @owner) + @updated = true if record - loaded - record + self.target = record end def updated? @@ -43,50 +19,52 @@ module ActiveRecord end private - def find_target - find_method = if @reflection.options[:primary_key] - "find_by_#{@reflection.options[:primary_key]}" - else - "find" - end - options = @reflection.options.dup.slice(:select, :include, :readonly) + def update_counters(record) + counter_cache_name = @reflection.counter_cache_column + + if counter_cache_name && @owner.persisted? && different_target?(record) + if record + record.class.increment_counter(counter_cache_name, record.id) + end - the_target = with_scope(:find => @scope[:find]) do - @reflection.klass.send(find_method, - @owner[@reflection.primary_key_name], - options - ) if @owner[@reflection.primary_key_name] + if foreign_key_present? + target_klass.decrement_counter(counter_cache_name, target_id) + end end - set_inverse_instance(the_target, @owner) - the_target end - def construct_find_scope - { :conditions => conditions } + # Checks whether record is different to the current target, without loading it + def different_target?(record) + record.nil? && @owner[@reflection.foreign_key] || + record.id != @owner[@reflection.foreign_key] end - def foreign_key_present - !@owner[@reflection.primary_key_name].nil? + def replace_keys(record) + @owner[@reflection.foreign_key] = record && record[@reflection.association_primary_key] + end + + def foreign_key_present? + @owner[@reflection.foreign_key] end # NOTE - for now, we're only supporting inverse setting from belongs_to back onto # has_one associations. - def we_can_set_the_inverse_on_this?(record) - @reflection.has_inverse? && @reflection.inverse_of.macro == :has_one + def invertible_for?(record) + inverse = inverse_reflection_for(record) + inverse && inverse.macro == :has_one end - def record_id(record) - record.send(@reflection.options[:primary_key] || :id) + def target_id + if @reflection.options[:primary_key] + @owner.send(@reflection.name).try(:id) + else + @owner[@reflection.foreign_key] + end end - def previous_record_id - @previous_record_id ||= if @reflection.options[:primary_key] - previous_record = @owner.send(@reflection.name) - previous_record.nil? ? nil : previous_record.id - else - @owner[@reflection.primary_key_name] - end + def stale_state + @owner[@reflection.foreign_key].to_s end end end diff --git a/activerecord/lib/active_record/associations/belongs_to_polymorphic_association.rb b/activerecord/lib/active_record/associations/belongs_to_polymorphic_association.rb index a0df860623..4f67b02d00 100644 --- a/activerecord/lib/active_record/associations/belongs_to_polymorphic_association.rb +++ b/activerecord/lib/active_record/associations/belongs_to_polymorphic_association.rb @@ -1,77 +1,33 @@ module ActiveRecord # = Active Record Belongs To Polymorphic Association module Associations - class BelongsToPolymorphicAssociation < AssociationProxy #:nodoc: - def replace(record) - if record.nil? - @target = @owner[@reflection.primary_key_name] = @owner[@reflection.options[:foreign_type]] = nil - else - @target = (AssociationProxy === record ? record.target : record) - - @owner[@reflection.primary_key_name] = record_id(record) - @owner[@reflection.options[:foreign_type]] = record.class.base_class.name.to_s - - @updated = true - end - - set_inverse_instance(record, @owner) - loaded - record - end - - def updated? - @updated - end - + class BelongsToPolymorphicAssociation < BelongsToAssociation #:nodoc: private - # NOTE - for now, we're only supporting inverse setting from belongs_to back onto - # has_one associations. - def we_can_set_the_inverse_on_this?(record) - if @reflection.has_inverse? - inverse_association = @reflection.polymorphic_inverse_of(record.class) - inverse_association && inverse_association.macro == :has_one - else - false - end - end - - def set_inverse_instance(record, instance) - return if record.nil? || !we_can_set_the_inverse_on_this?(record) - inverse_relationship = @reflection.polymorphic_inverse_of(record.class) - if inverse_relationship - record.send(:"set_#{inverse_relationship.name}_target", instance) - end + def replace_keys(record) + super + @owner[@reflection.foreign_type] = record && record.class.base_class.name end - def construct_find_scope - { :conditions => conditions } + def different_target?(record) + super || record.class != target_klass end - def find_target - return nil if association_class.nil? - - target = association_class.send(:with_scope, :find => @scope[:find]) do - association_class.find( - @owner[@reflection.primary_key_name], - :select => @reflection.options[:select], - :include => @reflection.options[:include] - ) - end - set_inverse_instance(target, @owner) - target + def inverse_reflection_for(record) + @reflection.polymorphic_inverse_of(record.class) end - def foreign_key_present - !@owner[@reflection.primary_key_name].nil? + def target_klass + type = @owner[@reflection.foreign_type] + type && type.constantize end - def record_id(record) - record.send(@reflection.options[:primary_key] || :id) + def raise_on_type_mismatch(record) + # A polymorphic association cannot have a type mismatch, by definition end - def association_class - @owner[@reflection.options[:foreign_type]] ? @owner[@reflection.options[:foreign_type]].constantize : nil + def stale_state + [super, @owner[@reflection.foreign_type].to_s] end end end diff --git a/activerecord/lib/active_record/associations/class_methods/join_dependency.rb b/activerecord/lib/active_record/associations/class_methods/join_dependency.rb index a74d0a4ca8..fdd4fe8946 100644 --- a/activerecord/lib/active_record/associations/class_methods/join_dependency.rb +++ b/activerecord/lib/active_record/associations/class_methods/join_dependency.rb @@ -176,7 +176,7 @@ module ActiveRecord join_part = join_parts.detect { |j| j.reflection.name.to_s == name && - j.parent_table_name == parent.class.table_name } + j.parent_table_name == parent.class.table_name } raise(ConfigurationError, "No such association") unless join_part @@ -201,7 +201,7 @@ module ActiveRecord macro = join_part.reflection.macro if macro == :has_one - return if record.instance_variable_defined?("@#{join_part.reflection.name}") + return if record.association_cache.key?(join_part.reflection.name) association = join_part.instantiate(row) unless row[join_part.aliased_primary_key].nil? set_target_and_inverse(join_part, association, record) else @@ -210,9 +210,9 @@ module ActiveRecord case macro when :has_many, :has_and_belongs_to_many collection = record.send(join_part.reflection.name) - collection.loaded + collection.loaded! collection.target.push(association) - collection.__send__(:set_inverse_instance, association, record) + collection.send(:set_inverse_instance, association) when :belongs_to set_target_and_inverse(join_part, association, record) else @@ -223,8 +223,9 @@ module ActiveRecord end def set_target_and_inverse(join_part, association, record) - association_proxy = record.send("set_#{join_part.reflection.name}_target", association) - association_proxy.__send__(:set_inverse_instance, association, record) + association_proxy = record.send(:association_proxy, join_part.reflection.name) + association_proxy.target = association + association_proxy.send(:set_inverse_instance, association) end end end diff --git a/activerecord/lib/active_record/associations/class_methods/join_dependency/join_association.rb b/activerecord/lib/active_record/associations/class_methods/join_dependency/join_association.rb index 694778008b..aaa475109e 100644 --- a/activerecord/lib/active_record/associations/class_methods/join_dependency/join_association.rb +++ b/activerecord/lib/active_record/associations/class_methods/join_dependency/join_association.rb @@ -82,12 +82,12 @@ module ActiveRecord connection = active_record.connection name = connection.table_alias_for "#{pluralize(reflection.name)}_#{parent_table_name}#{suffix}" - table_index = aliases[name] + 1 - name = name[0, connection.table_alias_length-3] + "_#{table_index}" if table_index > 1 + aliases[name] += 1 + name = name[0, connection.table_alias_length-3] + "_#{aliases[name]}" if aliases[name] > 1 + else + aliases[name] += 1 end - aliases[name] += 1 - name end @@ -108,6 +108,10 @@ module ActiveRecord end def process_conditions(conditions, table_name) + if conditions.respond_to?(:to_proc) + conditions = instance_eval(&conditions) + end + Arel.sql(sanitize_sql(conditions, table_name)) end @@ -190,17 +194,20 @@ module ActiveRecord ).alias @aliased_join_table_name jt_conditions = [] - jt_foreign_key = first_key = second_key = nil + first_key = second_key = nil - if through_reflection.options[:as] # has_many :through against a polymorphic join - as_key = through_reflection.options[:as].to_s - jt_foreign_key = as_key + '_id' - - jt_conditions << - join_table[as_key + '_type']. - eq(parent.active_record.base_class.name) + if through_reflection.macro == :belongs_to + jt_primary_key = through_reflection.foreign_key + jt_foreign_key = through_reflection.association_primary_key else - jt_foreign_key = through_reflection.primary_key_name + jt_primary_key = through_reflection.active_record_primary_key + jt_foreign_key = through_reflection.foreign_key + + if through_reflection.options[:as] # has_many :through against a polymorphic join + jt_conditions << + join_table["#{through_reflection.options[:as]}_type"]. + eq(parent.active_record.base_class.name) + end end case source_reflection.macro @@ -225,15 +232,15 @@ module ActiveRecord second_key = source_reflection.association_foreign_key jt_conditions << - join_table[reflection.source_reflection.options[:foreign_type]]. + join_table[reflection.source_reflection.foreign_type]. eq(reflection.options[:source_type]) else - second_key = source_reflection.primary_key_name + second_key = source_reflection.foreign_key end end jt_conditions << - parent_table[parent.primary_key]. + parent_table[jt_primary_key]. eq(join_table[jt_foreign_key]) if through_reflection.options[:conditions] @@ -259,7 +266,7 @@ module ActiveRecord end def join_belongs_to_to(relation) - foreign_key = options[:foreign_key] || reflection.primary_key_name + foreign_key = options[:foreign_key] || reflection.foreign_key primary_key = options[:primary_key] || reflection.klass.primary_key join_target_table( diff --git a/activerecord/lib/active_record/associations/has_and_belongs_to_many_association.rb b/activerecord/lib/active_record/associations/has_and_belongs_to_many_association.rb index e2ce9aefcf..b9c9919e7a 100644 --- a/activerecord/lib/active_record/associations/has_and_belongs_to_many_association.rb +++ b/activerecord/lib/active_record/associations/has_and_belongs_to_many_association.rb @@ -2,81 +2,48 @@ module ActiveRecord # = Active Record Has And Belongs To Many Association module Associations class HasAndBelongsToManyAssociation < AssociationCollection #:nodoc: - def create(attributes = {}) - create_record(attributes) { |record| insert_record(record) } - end - - def create!(attributes = {}) - create_record(attributes) { |record| insert_record(record, true) } - end - - def columns - @reflection.columns(@reflection.options[:join_table], "#{@reflection.options[:join_table]} Columns") - end - - def reset_column_information - @reflection.reset_column_information - end + attr_reader :join_table - def has_primary_key? - @has_primary_key ||= @owner.connection.supports_primary_key? && @owner.connection.primary_key(@reflection.options[:join_table]) + def initialize(owner, reflection) + @join_table = Arel::Table.new(reflection.options[:join_table]) + super end protected - def construct_find_options!(options) - options[:joins] = Arel::SqlLiteral.new(@scope[:find][:joins]) - options[:readonly] = finding_with_ambiguous_select?(options[:select] || @reflection.options[:select]) - options[:select] ||= (@reflection.options[:select] || Arel::SqlLiteral.new('*')) - end - def count_records - load_target.size - end - - def insert_record(record, force = true, validate = true) - if record.new_record? - if force - record.save! - else - return false unless record.save(:validate => validate) - end - end + def insert_record(record, validate = true) + return if record.new_record? && !record.save(:validate => validate) if @reflection.options[:insert_sql] - @owner.connection.insert(interpolate_sql(@reflection.options[:insert_sql], record)) + @owner.connection.insert(interpolate(@reflection.options[:insert_sql], record)) else - relation = Arel::Table.new(@reflection.options[:join_table]) - timestamps = record_timestamp_columns(record) - timezone = record.send(:current_time_from_proper_timezone) if timestamps.any? - - attributes = columns.map do |column| - name = column.name - value = case name.to_s - when @reflection.primary_key_name.to_s - @owner.id - when @reflection.association_foreign_key.to_s - record.id - when *timestamps - timezone - else - @owner.send(:quote_value, record[name], column) if record.has_attribute?(name) - end - [relation[name], value] unless value.nil? - end - - stmt = relation.compile_insert Hash[attributes] + stmt = join_table.compile_insert( + join_table[@reflection.foreign_key] => @owner.id, + join_table[@reflection.association_foreign_key] => record.id + ) + @owner.connection.insert stmt.to_sql end - true + record end - def delete_records(records) + def association_scope + super.joins(construct_joins) + end + + private + + def count_records + load_target.size + end + + def delete_records(records, method) if sql = @reflection.options[:delete_sql] - records.each { |record| @owner.connection.delete(interpolate_sql(sql, record)) } + records.each { |record| @owner.connection.delete(interpolate(sql, record)) } else - relation = Arel::Table.new(@reflection.options[:join_table]) - stmt = relation.where(relation[@reflection.primary_key_name].eq(@owner.id). + relation = join_table + stmt = relation.where(relation[@reflection.foreign_key].eq(@owner.id). and(relation[@reflection.association_foreign_key].in(records.map { |x| x.id }.compact)) ).compile_delete @owner.connection.delete stmt.to_sql @@ -84,51 +51,25 @@ module ActiveRecord end def construct_joins - "INNER JOIN #{@owner.connection.quote_table_name @reflection.options[:join_table]} ON #{@reflection.quoted_table_name}.#{@reflection.klass.primary_key} = #{@owner.connection.quote_table_name @reflection.options[:join_table]}.#{@reflection.association_foreign_key}" - end + right = join_table + left = @reflection.klass.arel_table - def construct_conditions - sql = "#{@owner.connection.quote_table_name @reflection.options[:join_table]}.#{@reflection.primary_key_name} = #{owner_quoted_id} " - sql << " AND (#{conditions})" if conditions - sql - end + condition = left[@reflection.klass.primary_key].eq( + right[@reflection.association_foreign_key]) - def construct_find_scope - { - :conditions => construct_conditions, - :joins => construct_joins, - :readonly => false, - :order => @reflection.options[:order], - :include => @reflection.options[:include], - :limit => @reflection.options[:limit] - } + right.create_join(right, right.create_on(condition)) end - # Join tables with additional columns on top of the two foreign keys must be considered - # ambiguous unless a select clause has been explicitly defined. Otherwise you can get - # broken records back, if, for example, the join column also has an id column. This will - # then overwrite the id column of the records coming back. - def finding_with_ambiguous_select?(select_clause) - !select_clause && columns.size != 2 + def construct_owner_conditions + super(join_table) end - private - def create_record(attributes, &block) - # Can't use Base.create because the foreign key may be a protected attribute. - ensure_owner_is_persisted! - if attributes.is_a?(Array) - attributes.collect { |attr| create(attr) } - else - build_record(attributes, &block) - end + def select_value + super || @reflection.klass.arel_table[Arel.star] end - def record_timestamp_columns(record) - if record.record_timestamps - record.send(:all_timestamp_attributes).map { |x| x.to_s } - else - [] - end + def invertible_for?(record) + false end end end diff --git a/activerecord/lib/active_record/associations/has_many_association.rb b/activerecord/lib/active_record/associations/has_many_association.rb index 4ff61fff45..543b073393 100644 --- a/activerecord/lib/active_record/associations/has_many_association.rb +++ b/activerecord/lib/active_record/associations/has_many_association.rb @@ -7,14 +7,14 @@ module ActiveRecord # is provided by its child HasManyThroughAssociation. class HasManyAssociation < AssociationCollection #:nodoc: protected - def owner_quoted_id - if @reflection.options[:primary_key] - @owner.class.quote_value(@owner.send(@reflection.options[:primary_key])) - else - @owner.quoted_id - end + + def insert_record(record, validate = true) + set_owner_attributes(record) + record.save(:validate => validate) end + private + # Returns the number of records in this collection. # # If the association has a counter cache it gets that value. Otherwise @@ -34,83 +34,69 @@ module ActiveRecord elsif @reflection.options[:counter_sql] || @reflection.options[:finder_sql] @reflection.klass.count_by_sql(custom_counter_sql) else - @reflection.klass.count(@scope[:find].slice(:conditions, :joins, :include)) + scoped.count end # If there's nothing in the database and @target has no new records # we are certain the current target is an empty array. This is a # documented side-effect of the method that may avoid an extra SELECT. - @target ||= [] and loaded if count == 0 + @target ||= [] and loaded! if count == 0 [@reflection.options[:limit], count].compact.min end - def has_cached_counter? - @owner.attribute_present?(cached_counter_attribute_name) - end - - def cached_counter_attribute_name - "#{@reflection.name}_count" + def has_cached_counter?(reflection = @reflection) + @owner.attribute_present?(cached_counter_attribute_name(reflection)) end - def insert_record(record, force = false, validate = true) - set_belongs_to_association_for(record) - force ? record.save! : record.save(:validate => validate) + def cached_counter_attribute_name(reflection = @reflection) + "#{reflection.name}_count" end - # Deletes the records according to the <tt>:dependent</tt> option. - def delete_records(records) - case @reflection.options[:dependent] - when :destroy - records.each { |r| r.destroy } - when :delete_all - @reflection.klass.delete(records.map { |record| record.id }) - else - relation = Arel::Table.new(@reflection.table_name) - stmt = relation.where(relation[@reflection.primary_key_name].eq(@owner.id). - and(relation[@reflection.klass.primary_key].in(records.map { |r| r.id })) - ).compile_update(relation[@reflection.primary_key_name] => nil) - @owner.connection.update stmt.to_sql - - @owner.class.update_counters(@owner.id, cached_counter_attribute_name => -records.size) if has_cached_counter? + def update_counter(difference, reflection = @reflection) + if has_cached_counter?(reflection) + counter = cached_counter_attribute_name(reflection) + @owner.class.update_counters(@owner.id, counter => difference) + @owner[counter] += difference + @owner.changed_attributes.delete(counter) # eww end end - def target_obsolete? - false + # This shit is nasty. We need to avoid the following situation: + # + # * An associated record is deleted via record.destroy + # * Hence the callbacks run, and they find a belongs_to on the record with a + # :counter_cache options which points back at our @owner. So they update the + # counter cache. + # * In which case, we must make sure to *not* update the counter cache, or else + # it will be decremented twice. + # + # Hence this method. + def inverse_updates_counter_cache?(reflection = @reflection) + counter_name = cached_counter_attribute_name(reflection) + reflection.klass.reflect_on_all_associations(:belongs_to).any? { |inverse_reflection| + inverse_reflection.counter_cache_column == counter_name + } end - def construct_conditions - if @reflection.options[:as] - sql = - "#{@reflection.quoted_table_name}.#{@reflection.options[:as]}_id = #{owner_quoted_id} AND " + - "#{@reflection.quoted_table_name}.#{@reflection.options[:as]}_type = #{@owner.class.quote_value(@owner.class.base_class.name.to_s)}" + # Deletes the records according to the <tt>:dependent</tt> option. + def delete_records(records, method) + if method == :destroy + records.each { |r| r.destroy } + update_counter(-records.length) unless inverse_updates_counter_cache? else - sql = "#{@reflection.quoted_table_name}.#{@reflection.primary_key_name} = #{owner_quoted_id}" - end - sql << " AND (#{conditions})" if conditions - sql - end - - def construct_find_scope - { - :conditions => construct_conditions, - :readonly => false, - :order => @reflection.options[:order], - :limit => @reflection.options[:limit], - :include => @reflection.options[:include] - } - end + keys = records.map { |r| r[@reflection.association_primary_key] } + scope = scoped.where(@reflection.association_primary_key => keys) - def construct_create_scope - create_scoping = {} - set_belongs_to_association_for(create_scoping) - create_scoping + if method == :delete_all + update_counter(-scope.delete_all) + else + update_counter(-scope.update_all(@reflection.foreign_key => nil)) + end + end end - def we_can_set_the_inverse_on_this?(record) - @reflection.inverse_of - end + alias creation_attributes construct_owner_attributes end end end 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 781aa7ef62..9f74d57c4d 100644 --- a/activerecord/lib/active_record/associations/has_many_through_association.rb +++ b/activerecord/lib/active_record/associations/has_many_through_association.rb @@ -1,82 +1,89 @@ -require "active_record/associations/through_association_scope" require 'active_support/core_ext/object/blank' module ActiveRecord # = Active Record Has Many Through Association module Associations class HasManyThroughAssociation < HasManyAssociation #:nodoc: - include ThroughAssociationScope + include ThroughAssociation alias_method :new, :build - def create!(attrs = nil) - create_record(attrs, true) - end - - def create(attrs = nil) - create_record(attrs, false) - end - - def destroy(*records) - transaction do - delete_records(flatten_deeper(records)) - super - end - end - # Returns the size of the collection by executing a SELECT COUNT(*) query if the collection hasn't been # loaded and calling collection.size if it has. If it's more likely than not that the collection does # have a size larger than zero, and you need to fetch that collection afterwards, it'll take one fewer # SELECT query if you use #length. def size - return @owner.send(:read_attribute, cached_counter_attribute_name) if has_cached_counter? - return @target.size if loaded? - return count + if has_cached_counter? + @owner.send(:read_attribute, cached_counter_attribute_name) + elsif loaded? + @target.size + else + count + end + end + + def <<(*records) + unless @owner.new_record? + records.flatten.each do |record| + raise_on_type_mismatch(record) + record.save! if record.new_record? + end + end + + super end protected - def create_record(attrs, force = true) - ensure_owner_is_persisted! - transaction do - object = @reflection.klass.new(attrs) - add_record_to_target_with_callbacks(object) {|r| insert_record(object, force) } - object - end + def insert_record(record, validate = true) + return if record.new_record? && !record.save(:validate => validate) + + through_association = @owner.send(@reflection.through_reflection.name) + through_association.create!(construct_join_attributes(record)) + + update_counter(1) + record end + private + def target_reflection_has_associated_record? - if @reflection.through_reflection.macro == :belongs_to && @owner[@reflection.through_reflection.primary_key_name].blank? + if @reflection.through_reflection.macro == :belongs_to && @owner[@reflection.through_reflection.foreign_key].blank? false else true end end - def construct_find_options!(options) - options[:joins] = [construct_joins] + Array.wrap(options[:joins]) - options[:include] = @reflection.source_reflection.options[:include] if options[:include].nil? && @reflection.source_reflection.options[:include] + def update_through_counter?(method) + case method + when :destroy + !inverse_updates_counter_cache?(@reflection.through_reflection) + when :nullify + false + else + true + end end - def insert_record(record, force = true, validate = true) - if record.new_record? - if force - record.save! - else - return false unless record.save(:validate => validate) - end - end + def delete_records(records, method) + through = @owner.send(:association_proxy, @reflection.through_reflection.name) + scope = through.scoped.where(construct_join_attributes(*records)) - through_association = @owner.send(@reflection.through_reflection.name) - through_association.create!(construct_join_attributes(record)) - end + case method + when :destroy + count = scope.destroy_all.length + when :nullify + count = scope.update_all(@reflection.source_reflection.foreign_key => nil) + else + count = scope.delete_all + end - # TODO - add dependent option support - def delete_records(records) - klass = @reflection.through_reflection.klass - records.each do |associate| - klass.delete_all(construct_join_attributes(associate)) + if @reflection.through_reflection.macro == :has_many && update_through_counter?(method) + update_counter(-count, @reflection.through_reflection) end + + update_counter(-count) end def find_target @@ -84,16 +91,8 @@ module ActiveRecord scoped.all end - def has_cached_counter? - @owner.attribute_present?(cached_counter_attribute_name) - end - - def cached_counter_attribute_name - "#{@reflection.name}_count" - end - # NOTE - not sure that we can actually cope with inverses here - def we_can_set_the_inverse_on_this?(record) + def invertible_for?(record) false end end diff --git a/activerecord/lib/active_record/associations/has_one_association.rb b/activerecord/lib/active_record/associations/has_one_association.rb index c49fd6e66a..a0828dcdea 100644 --- a/activerecord/lib/active_record/associations/has_one_association.rb +++ b/activerecord/lib/active_record/associations/has_one_association.rb @@ -1,135 +1,65 @@ module ActiveRecord # = Active Record Belongs To Has One Association module Associations - class HasOneAssociation < AssociationProxy #:nodoc: - def create(attrs = {}, replace_existing = true) - new_record(replace_existing) do |reflection| - attrs = merge_with_conditions(attrs) - reflection.create_association(attrs) - end - end - - def create!(attrs = {}, replace_existing = true) - new_record(replace_existing) do |reflection| - attrs = merge_with_conditions(attrs) - reflection.create_association!(attrs) - end - end + class HasOneAssociation < SingularAssociation #:nodoc: + def replace(record, save = true) + record = check_record(record) + load_target - def build(attrs = {}, replace_existing = true) - new_record(replace_existing) do |reflection| - attrs = merge_with_conditions(attrs) - reflection.build_association(attrs) - end - end + @reflection.klass.transaction do + if @target && @target != record + remove_target!(@reflection.options[:dependent]) + end - def replace(obj, dont_save = false) - load_target + if record + set_inverse_instance(record) + set_owner_attributes(record) - unless @target.nil? || @target == obj - if dependent? && !dont_save - case @reflection.options[:dependent] - when :delete - @target.delete if @target.persisted? - @owner.clear_association_cache - when :destroy - @target.destroy if @target.persisted? - @owner.clear_association_cache - when :nullify - @target[@reflection.primary_key_name] = nil - @target.save if @owner.persisted? && @target.persisted? + if @owner.persisted? && save && !record.save + nullify_owner_attributes(record) + set_owner_attributes(@target) + raise RecordNotSaved, "Failed to save the new associated #{@reflection.name}." end - else - @target[@reflection.primary_key_name] = nil - @target.save if @owner.persisted? && @target.persisted? end end - if obj.nil? - @target = nil - else - raise_on_type_mismatch(obj) - set_belongs_to_association_for(obj) - @target = (AssociationProxy === obj ? obj.target : obj) - end - - set_inverse_instance(obj, @owner) - @loaded = true - - unless !@owner.persisted? || obj.nil? || dont_save - return (obj.save ? self : false) - else - return (obj.nil? ? nil : self) - end + self.target = record end protected - def owner_quoted_id - if @reflection.options[:primary_key] - @owner.class.quote_value(@owner.send(@reflection.options[:primary_key])) - else - @owner.quoted_id - end + + def association_scope + super.order(@reflection.options[:order]) end private - def find_target - options = @reflection.options.dup.slice(:select, :order, :include, :readonly) - - the_target = with_scope(:find => @scope[:find]) do - @reflection.klass.find(:first, options) - end - set_inverse_instance(the_target, @owner) - the_target - end - def construct_find_scope - if @reflection.options[:as] - sql = - "#{@reflection.quoted_table_name}.#{@reflection.options[:as]}_id = #{owner_quoted_id} AND " + - "#{@reflection.quoted_table_name}.#{@reflection.options[:as]}_type = #{@owner.class.quote_value(@owner.class.base_class.name.to_s)}" - else - sql = "#{@reflection.quoted_table_name}.#{@reflection.primary_key_name} = #{owner_quoted_id}" - end - sql << " AND (#{conditions})" if conditions - { :conditions => sql } - end + alias creation_attributes construct_owner_attributes - def construct_create_scope - create_scoping = {} - set_belongs_to_association_for(create_scoping) - create_scoping + # The reason that the save param for replace is false, if for create (not just build), + # is because the setting of the foreign keys is actually handled by the scoping when + # the record is instantiated, and so they are set straight away and do not need to be + # updated within replace. + def set_new_record(record) + replace(record, false) end - def new_record(replace_existing) - # Make sure we load the target first, if we plan on replacing the existing - # instance. Otherwise, if the target has not previously been loaded - # elsewhere, the instance we create will get orphaned. - load_target if replace_existing - record = @reflection.klass.send(:with_scope, :create => @scope[:create]) do - yield @reflection - end - - if replace_existing - replace(record, true) + def remove_target!(method) + if [:delete, :destroy].include?(method) + @target.send(method) else - record[@reflection.primary_key_name] = @owner.id if @owner.persisted? - self.target = record - set_inverse_instance(record, @owner) - end + nullify_owner_attributes(@target) - record - end - - def we_can_set_the_inverse_on_this?(record) - inverse = @reflection.inverse_of - return !inverse.nil? + if @target.persisted? && @owner.persisted? && !@target.save + set_owner_attributes(@target) + raise RecordNotSaved, "Failed to remove the existing associated #{@reflection.name}. " + + "The record failed to save when after its foreign key was set to nil." + end + end end - def merge_with_conditions(attrs={}) - attrs ||= {} - attrs.update(@reflection.options[:conditions]) if @reflection.options[:conditions].is_a?(Hash) - attrs + def nullify_owner_attributes(record) + record[@reflection.foreign_key] = nil end end end diff --git a/activerecord/lib/active_record/associations/has_one_through_association.rb b/activerecord/lib/active_record/associations/has_one_through_association.rb index e8cf73976b..69771afe50 100644 --- a/activerecord/lib/active_record/associations/has_one_through_association.rb +++ b/activerecord/lib/active_record/associations/has_one_through_association.rb @@ -1,40 +1,34 @@ -require "active_record/associations/through_association_scope" - module ActiveRecord # = Active Record Has One Through Association module Associations class HasOneThroughAssociation < HasOneAssociation - include ThroughAssociationScope + include ThroughAssociation - def replace(new_value) - create_through_record(new_value) - @target = new_value + def replace(record) + create_through_record(record) + self.target = record end private - def create_through_record(new_value) #nodoc: - klass = @reflection.through_reflection.klass + def create_through_record(record) + through_proxy = @owner.send(:association_proxy, @reflection.through_reflection.name) + through_record = through_proxy.send(:load_target) - current_object = @owner.send(@reflection.through_reflection.name) + if through_record && !record + through_record.destroy + elsif record + attributes = construct_join_attributes(record) - if current_object - new_value ? current_object.update_attributes(construct_join_attributes(new_value)) : current_object.destroy - elsif new_value - if @owner.new_record? - self.target = new_value - through_association = @owner.send(:association_instance_get, @reflection.through_reflection.name) - through_association.build(construct_join_attributes(new_value)) - else - @owner.send(@reflection.through_reflection.name, klass.create(construct_join_attributes(new_value))) + if through_record + through_record.update_attributes(attributes) + elsif @owner.new_record? + through_proxy.build(attributes) + else + through_proxy.create(attributes) + end end end - end - - private - def find_target - scoped.first - end end end end diff --git a/activerecord/lib/active_record/associations/singular_association.rb b/activerecord/lib/active_record/associations/singular_association.rb new file mode 100644 index 0000000000..7f92d9712a --- /dev/null +++ b/activerecord/lib/active_record/associations/singular_association.rb @@ -0,0 +1,45 @@ +module ActiveRecord + module Associations + class SingularAssociation < AssociationProxy #:nodoc: + def create(attributes = {}) + new_record(:create, attributes) + end + + def create!(attributes = {}) + build(attributes).tap { |record| record.save! } + end + + def build(attributes = {}) + new_record(:build, attributes) + end + + private + + def find_target + scoped.first.tap { |record| set_inverse_instance(record) } + end + + # Implemented by subclasses + def replace(record) + raise NotImplementedError + end + + def set_new_record(record) + replace(record) + end + + def check_record(record) + record = record.target if AssociationProxy === record + raise_on_type_mismatch(record) if record + record + end + + def new_record(method, attributes) + attributes = scoped.scope_for_create.merge(attributes || {}) + record = @reflection.send("#{method}_association", attributes) + set_new_record(record) + record + end + end + end +end diff --git a/activerecord/lib/active_record/associations/through_association.rb b/activerecord/lib/active_record/associations/through_association.rb new file mode 100644 index 0000000000..0550bae408 --- /dev/null +++ b/activerecord/lib/active_record/associations/through_association.rb @@ -0,0 +1,156 @@ +module ActiveRecord + # = Active Record Through Association + module Associations + module ThroughAssociation + + protected + + def target_scope + super.merge(@reflection.through_reflection.klass.scoped) + end + + def association_scope + scope = super.joins(construct_joins) + scope = add_conditions(scope) + unless @reflection.options[:include] + scope = scope.includes(@reflection.source_reflection.options[:include]) + end + scope + end + + private + + # This scope affects the creation of the associated records (not the join records). At the + # moment we only support creating on a :through association when the source reflection is a + # belongs_to. Thus it's not necessary to set a foreign key on the associated record(s), so + # this scope has can legitimately be empty. + def creation_attributes + { } + end + + def aliased_through_table + name = @reflection.through_reflection.table_name + + @reflection.table_name == name ? + @reflection.through_reflection.klass.arel_table.alias(name + "_join") : + @reflection.through_reflection.klass.arel_table + end + + def construct_owner_conditions + super(aliased_through_table, @reflection.through_reflection) + end + + def construct_joins + right = aliased_through_table + left = @reflection.klass.arel_table + + conditions = [] + + if @reflection.source_reflection.macro == :belongs_to + reflection_primary_key = @reflection.source_reflection.association_primary_key + source_primary_key = @reflection.source_reflection.foreign_key + + if @reflection.options[:source_type] + column = @reflection.source_reflection.foreign_type + conditions << + right[column].eq(@reflection.options[:source_type]) + end + else + reflection_primary_key = @reflection.source_reflection.foreign_key + source_primary_key = @reflection.source_reflection.active_record_primary_key + + if @reflection.source_reflection.options[:as] + column = "#{@reflection.source_reflection.options[:as]}_type" + conditions << + left[column].eq(@reflection.through_reflection.klass.name) + end + end + + conditions << + left[reflection_primary_key].eq(right[source_primary_key]) + + right.create_join( + right, + right.create_on(right.create_and(conditions))) + end + + # Construct attributes for :through pointing to owner and associate. This is used by the + # methods which create and delete records on the association. + # + # We only support indirectly modifying through associations which has a belongs_to source. + # This is the "has_many :tags, :through => :taggings" situation, where the join model + # typically has a belongs_to on both side. In other words, associations which could also + # be represented as has_and_belongs_to_many associations. + # + # We do not support creating/deleting records on the association where the source has + # some other type, because this opens up a whole can of worms, and in basically any + # situation it is more natural for the user to just create or modify their join records + # directly as required. + def construct_join_attributes(*records) + if @reflection.source_reflection.macro != :belongs_to + raise HasManyThroughCantAssociateThroughHasOneOrManyReflection.new(@owner, @reflection) + end + + join_attributes = { + @reflection.source_reflection.foreign_key => + records.map { |record| + record.send(@reflection.source_reflection.association_primary_key) + } + } + + if @reflection.options[:source_type] + join_attributes[@reflection.source_reflection.foreign_type] = + records.map { |record| record.class.base_class.name } + end + + if records.count == 1 + Hash[join_attributes.map { |k, v| [k, v.first] }] + else + join_attributes + end + end + + # The reason that we are operating directly on the scope here (rather than passing + # back some arel conditions to be added to the scope) is because scope.where([x, y]) + # has a different meaning to scope.where(x).where(y) - the first version might + # perform some substitution if x is a string. + def add_conditions(scope) + unless @reflection.through_reflection.klass.descends_from_active_record? + scope = scope.where(@reflection.through_reflection.klass.send(:type_condition)) + end + + scope = scope.where(interpolate(@reflection.source_reflection.options[:conditions])) + scope.where(through_conditions) + end + + # If there is a hash of conditions then we make sure the keys are scoped to the + # through table name if left ambiguous. + def through_conditions + conditions = interpolate(@reflection.through_reflection.options[:conditions]) + + if conditions.is_a?(Hash) + Hash[conditions.map { |key, value| + unless value.is_a?(Hash) || key.to_s.include?('.') + key = aliased_through_table.name + '.' + key.to_s + end + + [key, value] + }] + else + conditions + end + end + + def stale_state + if @reflection.through_reflection.macro == :belongs_to + @owner[@reflection.through_reflection.foreign_key].to_s + end + end + + def foreign_key_present? + @reflection.through_reflection.macro == :belongs_to && + !@owner[@reflection.through_reflection.foreign_key].nil? + end + end + end +end diff --git a/activerecord/lib/active_record/associations/through_association_scope.rb b/activerecord/lib/active_record/associations/through_association_scope.rb deleted file mode 100644 index 5dc5b0c048..0000000000 --- a/activerecord/lib/active_record/associations/through_association_scope.rb +++ /dev/null @@ -1,165 +0,0 @@ -module ActiveRecord - # = Active Record Through Association Scope - module Associations - module ThroughAssociationScope - - def scoped - with_scope(@scope) do - @reflection.klass.scoped & - @reflection.through_reflection.klass.scoped - end - end - - protected - - def construct_find_scope - { - :conditions => construct_conditions, - :joins => construct_joins, - :include => @reflection.options[:include] || @reflection.source_reflection.options[:include], - :select => construct_select, - :order => @reflection.options[:order], - :limit => @reflection.options[:limit], - :readonly => @reflection.options[:readonly] - } - end - - def construct_create_scope - construct_owner_attributes(@reflection) - end - - def aliased_through_table - name = @reflection.through_reflection.table_name - - @reflection.table_name == name ? - @reflection.through_reflection.klass.arel_table.alias(name + "_join") : - @reflection.through_reflection.klass.arel_table - end - - # Build SQL conditions from attributes, qualified by table name. - def construct_conditions - table = aliased_through_table - conditions = construct_owner_attributes(@reflection.through_reflection).map do |attr, value| - table[attr].eq(value) - end - conditions << Arel.sql(sql_conditions) if sql_conditions - table.create_and(conditions) - end - - # Associate attributes pointing to owner - def construct_owner_attributes(reflection) - if as = reflection.options[:as] - { "#{as}_id" => @owner[reflection.active_record_primary_key], - "#{as}_type" => @owner.class.base_class.name } - elsif reflection.macro == :belongs_to - { reflection.klass.primary_key => @owner[reflection.primary_key_name] } - else - { reflection.primary_key_name => @owner[reflection.active_record_primary_key] } - end - end - - def construct_from - @reflection.table_name - end - - def construct_select(custom_select = nil) - distinct = "DISTINCT " if @reflection.options[:uniq] - custom_select || @reflection.options[:select] || "#{distinct}#{@reflection.quoted_table_name}.*" - end - - def construct_joins - right = aliased_through_table - left = @reflection.klass.arel_table - - conditions = [] - - if @reflection.source_reflection.macro == :belongs_to - reflection_primary_key = @reflection.source_reflection.options[:primary_key] || - @reflection.klass.primary_key - source_primary_key = @reflection.source_reflection.primary_key_name - if @reflection.options[:source_type] - column = @reflection.source_reflection.options[:foreign_type] - conditions << - right[column].eq(@reflection.options[:source_type]) - end - else - reflection_primary_key = @reflection.source_reflection.primary_key_name - source_primary_key = @reflection.source_reflection.options[:primary_key] || - @reflection.through_reflection.klass.primary_key - if @reflection.source_reflection.options[:as] - column = "#{@reflection.source_reflection.options[:as]}_type" - conditions << - left[column].eq(@reflection.through_reflection.klass.name) - end - end - - conditions << - left[reflection_primary_key].eq(right[source_primary_key]) - - right.create_join( - right, - right.create_on(right.create_and(conditions))) - end - - # Construct attributes for :through pointing to owner and associate. - def construct_join_attributes(associate) - # TODO: revisit this to allow it for deletion, supposing dependent option is supported - raise ActiveRecord::HasManyThroughCantAssociateThroughHasOneOrManyReflection.new(@owner, @reflection) if [:has_one, :has_many].include?(@reflection.source_reflection.macro) - - join_attributes = construct_owner_attributes(@reflection.through_reflection).merge(@reflection.source_reflection.primary_key_name => associate.id) - - if @reflection.options[:source_type] - join_attributes.merge!(@reflection.source_reflection.options[:foreign_type] => associate.class.base_class.name) - end - - if @reflection.through_reflection.options[:conditions].is_a?(Hash) - join_attributes.merge!(@reflection.through_reflection.options[:conditions]) - end - - join_attributes - end - - def conditions - @conditions = build_conditions unless defined?(@conditions) - @conditions - end - - def build_conditions - association_conditions = @reflection.options[:conditions] - through_conditions = build_through_conditions - source_conditions = @reflection.source_reflection.options[:conditions] - uses_sti = !@reflection.through_reflection.klass.descends_from_active_record? - - if association_conditions || through_conditions || source_conditions || uses_sti - all = [] - - [association_conditions, source_conditions].each do |conditions| - all << interpolate_sql(sanitize_sql(conditions)) if conditions - end - - all << through_conditions if through_conditions - all << build_sti_condition if uses_sti - - all.map { |sql| "(#{sql})" } * ' AND ' - end - end - - def build_through_conditions - conditions = @reflection.through_reflection.options[:conditions] - if conditions.is_a?(Hash) - interpolate_sql(@reflection.through_reflection.klass.send(:sanitize_sql, conditions)).gsub( - @reflection.quoted_table_name, - @reflection.through_reflection.quoted_table_name) - elsif conditions - interpolate_sql(sanitize_sql(conditions)) - end - end - - def build_sti_condition - @reflection.through_reflection.klass.send(:type_condition).to_sql - end - - alias_method :sql_conditions, :conditions - end - end -end diff --git a/activerecord/lib/active_record/attribute_methods/primary_key.rb b/activerecord/lib/active_record/attribute_methods/primary_key.rb index 75ae06f5e9..fcdd31ddea 100644 --- a/activerecord/lib/active_record/attribute_methods/primary_key.rb +++ b/activerecord/lib/active_record/attribute_methods/primary_key.rb @@ -14,26 +14,37 @@ module ActiveRecord # Defines the primary key field -- can be overridden in subclasses. Overwriting will negate any effect of the # primary_key_prefix_type setting, though. def primary_key - reset_primary_key + @primary_key ||= reset_primary_key end def reset_primary_key #:nodoc: - key = get_primary_key(base_class.name) + key = self == base_class ? get_primary_key(base_class.name) : + base_class.primary_key + set_primary_key(key) key end def get_primary_key(base_name) #:nodoc: - key = 'id' + return 'id' unless base_name && !base_name.blank? + case primary_key_prefix_type - when :table_name - key = base_name.to_s.foreign_key(false) - when :table_name_with_underscore - key = base_name.to_s.foreign_key + when :table_name + base_name.foreign_key(false) + when :table_name_with_underscore + base_name.foreign_key + else + if ActiveRecord::Base != self && connection.table_exists?(table_name) + connection.primary_key(table_name) + else + 'id' + end end - key end + attr_accessor :original_primary_key + attr_writer :primary_key + # Sets the name of the primary key column to use to the given value, # or (if the value is nil or false) to the value returned by the given # block. @@ -42,9 +53,12 @@ module ActiveRecord # set_primary_key "sysid" # end def set_primary_key(value = nil, &block) - define_attr_method :primary_key, value, &block + @primary_key ||= '' + self.original_primary_key = @primary_key + value &&= value.to_s + connection_pool.primary_keys[table_name] = value + self.primary_key = block_given? ? instance_eval(&block) : value end - alias :primary_key= :set_primary_key end end end diff --git a/activerecord/lib/active_record/attribute_methods/read.rb b/activerecord/lib/active_record/attribute_methods/read.rb index 506f6e878f..ab86d8bad1 100644 --- a/activerecord/lib/active_record/attribute_methods/read.rb +++ b/activerecord/lib/active_record/attribute_methods/read.rb @@ -20,7 +20,7 @@ module ActiveRecord # be cached. Usually caching only pays off for attributes with expensive conversion # methods, like time related columns (e.g. +created_at+, +updated_at+). def cache_attributes(*attribute_names) - attribute_names.each {|attr| cached_attributes << attr.to_s} + cached_attributes.merge attribute_names.map { |attr| attr.to_s } end # Returns the attributes which are cached. By default time related columns @@ -39,7 +39,7 @@ module ActiveRecord if serialized_attributes.include?(attr_name) define_read_method_for_serialized_attribute(attr_name) else - define_read_method(attr_name.to_sym, attr_name, columns_hash[attr_name]) + define_read_method(attr_name, attr_name, columns_hash[attr_name]) end if attr_name == primary_key && attr_name != "id" @@ -54,17 +54,17 @@ module ActiveRecord # Define read method for serialized attribute. def define_read_method_for_serialized_attribute(attr_name) - access_code = "@attributes_cache['#{attr_name}'] ||= unserialize_attribute('#{attr_name}')" - generated_attribute_methods.module_eval("def #{attr_name}; #{access_code}; end", __FILE__, __LINE__) + access_code = "@attributes_cache['#{attr_name}'] ||= @attributes['#{attr_name}']" + generated_attribute_methods.module_eval("def _#{attr_name}; #{access_code}; end; alias #{attr_name} _#{attr_name}", __FILE__, __LINE__) end # Define an attribute reader method. Cope with nil column. def define_read_method(symbol, attr_name, column) - cast_code = column.type_cast_code('v') if column - access_code = cast_code ? "(v=@attributes['#{attr_name}']) && #{cast_code}" : "@attributes['#{attr_name}']" + cast_code = column.type_cast_code('v') + access_code = "(v=@attributes['#{attr_name}']) && #{cast_code}" unless attr_name.to_s == self.primary_key.to_s - access_code = access_code.insert(0, "missing_attribute('#{attr_name}', caller) unless @attributes.has_key?('#{attr_name}'); ") + access_code.insert(0, "missing_attribute('#{attr_name}', caller) unless @attributes.has_key?('#{attr_name}'); ") end if cache_attribute?(attr_name) @@ -106,14 +106,10 @@ module ActiveRecord # Returns the unserialized object of the attribute. def unserialize_attribute(attr_name) - unserialized_object = object_from_yaml(@attributes[attr_name]) + coder = self.class.serialized_attributes[attr_name] + unserialized_object = coder.load(@attributes[attr_name]) - if unserialized_object.is_a?(self.class.serialized_attributes[attr_name]) || unserialized_object.nil? - @attributes.frozen? ? unserialized_object : @attributes[attr_name] = unserialized_object - else - raise SerializationTypeMismatch, - "#{attr_name} was supposed to be a #{self.class.serialized_attributes[attr_name]}, but was a #{unserialized_object.class.to_s}" - end + @attributes.frozen? ? unserialized_object : @attributes[attr_name] = unserialized_object end private diff --git a/activerecord/lib/active_record/attribute_methods/time_zone_conversion.rb b/activerecord/lib/active_record/attribute_methods/time_zone_conversion.rb index dc2785b6bf..76218d2a73 100644 --- a/activerecord/lib/active_record/attribute_methods/time_zone_conversion.rb +++ b/activerecord/lib/active_record/attribute_methods/time_zone_conversion.rb @@ -40,12 +40,13 @@ module ActiveRecord def define_method_attribute=(attr_name) if create_time_zone_conversion_attribute?(attr_name, columns_hash[attr_name]) method_body, line = <<-EOV, __LINE__ + 1 - def #{attr_name}=(time) + def #{attr_name}=(original_time) + time = original_time.dup unless original_time.nil? unless time.acts_like?(:time) time = time.is_a?(String) ? Time.zone.parse(time) : time.to_time rescue time end time = time.in_time_zone rescue nil if time - write_attribute(:#{attr_name}, time) + write_attribute(:#{attr_name}, (time || original_time)) end EOV generated_attribute_methods.module_eval(method_body, __FILE__, line) diff --git a/activerecord/lib/active_record/autosave_association.rb b/activerecord/lib/active_record/autosave_association.rb index 2dd69bd12c..ec27fd2e63 100644 --- a/activerecord/lib/active_record/autosave_association.rb +++ b/activerecord/lib/active_record/autosave_association.rb @@ -321,27 +321,30 @@ module ActiveRecord if records = associated_records_to_validate_or_save(association, @new_record_before_save, autosave) begin - records.each do |record| - next if record.destroyed? - - if autosave && record.marked_for_destruction? - association.destroy(record) - elsif autosave != false && (@new_record_before_save || record.new_record?) - if autosave - saved = association.send(:insert_record, record, false, false) - else - association.send(:insert_record, record) - end - elsif autosave - saved = record.save(:validate => false) + records.each do |record| + next if record.destroyed? + + saved = true + + if autosave && record.marked_for_destruction? + association.destroy(record) + elsif autosave != false && (@new_record_before_save || record.new_record?) + if autosave + saved = association.send(:insert_record, record, false) + else + association.send(:insert_record, record) end - - raise ActiveRecord::Rollback if saved == false + elsif autosave + saved = record.save(:validate => false) end + + raise ActiveRecord::Rollback unless saved + end rescue records.each {|x| IdentityMap.remove(x) } if IdentityMap.enabled? raise end + end # reconstruct the scope now that we know the owner's id @@ -365,8 +368,8 @@ module ActiveRecord association.destroy else 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 + if autosave != false && (new_record? || association.new_record? || association[reflection.foreign_key] != key || autosave) + association[reflection.foreign_key] = key saved = association.save(:validate => !autosave) raise ActiveRecord::Rollback if !saved && autosave saved @@ -389,7 +392,8 @@ module ActiveRecord if association.updated? association_id = association.send(reflection.options[:primary_key] || :id) - self[reflection.primary_key_name] = association_id + self[reflection.foreign_key] = association_id + association.loaded! end saved if autosave diff --git a/activerecord/lib/active_record/base.rb b/activerecord/lib/active_record/base.rb index 24c87662b8..01f5f4eccd 100644 --- a/activerecord/lib/active_record/base.rb +++ b/activerecord/lib/active_record/base.rb @@ -1,3 +1,8 @@ +begin + require 'psych' +rescue LoadError +end + require 'yaml' require 'set' require 'active_support/benchmarkable' @@ -244,6 +249,17 @@ module ActiveRecord #:nodoc: # user = User.create(:preferences => %w( one two three )) # User.find(user.id).preferences # raises SerializationTypeMismatch # + # When you specify a class option, the default value for that attribute will be a new + # instance of that class. + # + # class User < ActiveRecord::Base + # serialize :preferences, OpenStruct + # end + # + # user = User.new + # user.preferences.theme_color = "red" + # + # # == Single table inheritance # # Active Record allows inheritance by storing the name of the class in a column that by @@ -531,7 +547,15 @@ module ActiveRecord #:nodoc: # serialize :preferences # end def serialize(attr_name, class_name = Object) - serialized_attributes[attr_name.to_s] = class_name + coder = if [:load, :dump].all? { |x| class_name.respond_to?(x) } + class_name + else + Coders::YAMLColumn.new(class_name) + end + + # merge new serialized attribute and create new hash to ensure that each class in inheritance hierarchy + # has its own hash of own serialized attributes + self.serialized_attributes = serialized_attributes.merge(attr_name.to_s => coder) end # Guesses the table name (in forced lower-case) based on the name of the class in the @@ -614,6 +638,9 @@ module ActiveRecord #:nodoc: def set_table_name(value = nil, &block) @quoted_table_name = nil define_attr_method :table_name, value, &block + + @arel_table = Arel::Table.new(table_name, :engine => arel_engine) + @relation = Relation.new(self, arel_table) end alias :table_name= :set_table_name @@ -657,16 +684,12 @@ module ActiveRecord #:nodoc: # Returns an array of column objects for the table associated with this class. def columns - unless defined?(@columns) && @columns - @columns = connection.columns(table_name, "#{name} Columns") - @columns.each { |column| column.primary = column.name == primary_key } - end - @columns + connection_pool.columns[table_name] end # Returns a hash of column objects for the table associated with this class. def columns_hash - @columns_hash ||= Hash[columns.map { |column| [column.name, column] }] + connection_pool.columns_hash[table_name] end # Returns an array of column names as strings. @@ -723,8 +746,14 @@ module ActiveRecord #:nodoc: def reset_column_information connection.clear_cache! undefine_attribute_methods - @column_names = @columns = @columns_hash = @content_columns = @dynamic_methods_hash = @inheritance_column = nil - @arel_engine = @relation = @arel_table = nil + connection_pool.clear_table_cache!(table_name) if table_exists? + + @column_names = @content_columns = @dynamic_methods_hash = @inheritance_column = nil + @arel_engine = @relation = nil + end + + def clear_cache! # :nodoc: + connection_pool.clear_cache! end def attribute_method?(attribute) @@ -762,7 +791,7 @@ module ActiveRecord #:nodoc: :true == (@finder_needs_type_condition ||= descends_from_active_record? ? :false : :true) end - # Returns a string like 'Post id:integer, title:string, body:text' + # Returns a string like 'Post(id:integer, title:string, body:text)' def inspect if self == Base super @@ -827,13 +856,13 @@ module ActiveRecord #:nodoc: end def arel_table - @arel_table ||= Arel::Table.new(table_name, arel_engine) + Arel::Table.new(table_name, arel_engine) end def arel_engine @arel_engine ||= begin if self == ActiveRecord::Base - Arel::Table.engine + ActiveRecord::Base else connection_handler.connection_pools[name] ? self : superclass.arel_engine end @@ -874,35 +903,51 @@ module ActiveRecord #:nodoc: reset_scoped_methods end - private + # Specifies how the record is loaded by +Marshal+. + # + # +_load+ sets an instance variable for each key in the hash it takes as input. + # Override this method if you require more complex marshalling. + def _load(data) + record = allocate + record.init_with(Marshal.load(data)) + record + end - def relation #:nodoc: - @relation ||= Relation.new(self, arel_table) - finder_needs_type_condition? ? @relation.where(type_condition) : @relation - end - # Finder methods must instantiate through this method to work with the - # single-table inheritance model that makes it possible to create - # objects of different types from the same table. - def instantiate(record) - sti_class = find_sti_class(record[inheritance_column]) - record_id = sti_class.primary_key && record[sti_class.primary_key] + # Finder methods must instantiate through this method to work with the + # single-table inheritance model that makes it possible to create + # objects of different types from the same table. + def instantiate(record) + sti_class = find_sti_class(record[inheritance_column]) + record_id = sti_class.primary_key && record[sti_class.primary_key] - if ActiveRecord::IdentityMap.enabled? && record_id - if (column = sti_class.columns_hash[sti_class.primary_key]) && column.number? - record_id = record_id.to_i - end - if instance = IdentityMap.get(sti_class, record_id) - instance.reinit_with('attributes' => record) - else - instance = sti_class.allocate.init_with('attributes' => record) - IdentityMap.add(instance) - end + if ActiveRecord::IdentityMap.enabled? && record_id + if (column = sti_class.columns_hash[sti_class.primary_key]) && column.number? + record_id = record_id.to_i + end + if instance = IdentityMap.get(sti_class, record_id) + instance.reinit_with('attributes' => record) else instance = sti_class.allocate.init_with('attributes' => record) + IdentityMap.add(instance) end + else + instance = sti_class.allocate.init_with('attributes' => record) + end + + instance + end - instance + private + + def relation #:nodoc: + @relation ||= Relation.new(self, arel_table) + + if finder_needs_type_condition? + @relation.where(type_condition).create_with(inheritance_column.to_sym => sti_name) + else + @relation + end end def find_sti_class(type_name) @@ -932,11 +977,10 @@ module ActiveRecord #:nodoc: end def type_condition - sti_column = arel_table[inheritance_column] - condition = sti_column.eq(sti_name) - descendants.each { |subclass| condition = condition.or(sti_column.eq(subclass.sti_name)) } + sti_column = arel_table[inheritance_column.to_sym] + sti_names = ([self] + descendants).map { |model| model.sti_name } - condition + sti_column.in(sti_names) end # Guesses the table name, but does not decorate it with prefix and suffix information. @@ -1383,6 +1427,8 @@ MSG # hence you can't have attributes that aren't part of the table columns. def initialize(attributes = nil) @attributes = attributes_from_column_definition + @association_cache = {} + @aggregation_cache = {} @attributes_cache = {} @new_record = true @readonly = false @@ -1392,15 +1438,32 @@ MSG @changed_attributes = {} ensure_proper_type + set_serialized_attributes populate_with_current_scope_attributes self.attributes = attributes unless attributes.nil? result = yield self if block_given? - _run_initialize_callbacks + run_callbacks :initialize result end + # Populate +coder+ with attributes about this record that should be + # serialized. The structure of +coder+ defined in this method is + # guaranteed to match the structure of +coder+ passed to the +init_with+ + # method. + # + # Example: + # + # class Post < ActiveRecord::Base + # end + # coder = {} + # Post.new.encode_with(coder) + # coder # => { 'id' => nil, ... } + def encode_with(coder) + coder['attributes'] = attributes + end + # Initialize an empty model object from +coder+. +coder+ must contain # the attributes necessary for initializing an empty model object. For # example: @@ -1413,15 +1476,30 @@ MSG # post.title # => 'hello world' def init_with(coder) @attributes = coder['attributes'] + + set_serialized_attributes + @attributes_cache, @previously_changed, @changed_attributes = {}, {}, {} + @association_cache = {} + @aggregation_cache = {} @readonly = @destroyed = @marked_for_destruction = false @new_record = false - _run_find_callbacks - _run_initialize_callbacks + run_callbacks :find + run_callbacks :initialize self end + # Specifies how the record is dumped by +Marshal+. + # + # +_dump+ emits a marshalled hash which has been passed to +encode_with+. Override this + # method if you require more complex marshalling. + def _dump(level) + dump = {} + encode_with(dump) + Marshal.dump(dump) + end + # Returns a String, which Action Pack uses for constructing an URL to this # object. The default implementation returns this record's id as a String, # or nil if this record's unsaved. @@ -1511,8 +1589,10 @@ MSG attributes.each do |k, v| if k.include?("(") multi_parameter_attributes << [ k, v ] + elsif respond_to?("#{k}=") + send("#{k}=", v) else - respond_to?(:"#{k}=") ? send(:"#{k}=", v) : raise(UnknownAttributeError, "unknown attribute: #{k}") + raise(UnknownAttributeError, "unknown attribute: #{k}") end end @@ -1552,7 +1632,7 @@ MSG # Returns true if the specified +attribute+ has been set by the user or by a database load and is neither # nil nor empty? (the latter only applies to objects that respond to empty?, most notably Strings). def attribute_present?(attribute) - !read_attribute(attribute).blank? + !_read_attribute(attribute).blank? end # Returns the column object for the named attribute. @@ -1625,9 +1705,9 @@ MSG @changed_attributes[attr] = orig_value if field_changed?(attr, orig_value, @attributes[attr]) end - clear_aggregation_cache - clear_association_cache - @attributes_cache = {} + @aggregation_cache = {} + @association_cache = {} + @attributes_cache = {} @new_record = true ensure_proper_type @@ -1649,7 +1729,7 @@ MSG # Returns the contents of the record as a nicely formatted string. def inspect attributes_as_nice_string = self.class.column_names.collect { |name| - if has_attribute?(name) || new_record? + if has_attribute?(name) "#{name}: #{attribute_for_inspect(name)}" end }.compact.join(", ") @@ -1673,6 +1753,13 @@ MSG private + def set_serialized_attributes + (@attributes.keys & self.class.serialized_attributes.keys).each do |key| + coder = self.class.serialized_attributes[key] + @attributes[key] = coder.load @attributes[key] + end + end + # Sets the attribute used for single table inheritance to this class name if this is not the # ActiveRecord::Base descendant. # Considering the hierarchy Reply < Message < ActiveRecord::Base, this makes it possible to @@ -1694,17 +1781,25 @@ MSG # Returns a copy of the attributes hash where all the values have been safely quoted for use in # an Arel insert/update method. def arel_attributes_values(include_primary_key = true, include_readonly_attributes = true, attribute_names = @attributes.keys) - attrs = {} + attrs = {} + klass = self.class + arel_table = klass.arel_table + attribute_names.each do |name| if (column = column_for_attribute(name)) && (include_primary_key || !column.primary) if include_readonly_attributes || (!include_readonly_attributes && !self.class.readonly_attributes.include?(name)) - value = read_attribute(name) - if !value.nil? && self.class.serialized_attributes.key?(name) - value = YAML.dump value - end - attrs[self.class.arel_table[name]] = value + value = if coder = klass.serialized_attributes[name] + coder.dump @attributes[name] + else + # FIXME: we need @attributes to be used consistently. + # If the values stored in @attributes were already type + # casted, this code could be simplified + read_attribute(name) + end + + attrs[arel_table[name]] = value end end end @@ -1716,12 +1811,6 @@ MSG self.class.connection.quote(value, column) end - # Interpolate custom SQL string in instance context. - # Optional record argument is meant for custom insert_sql. - def interpolate_sql(sql, record = nil) - instance_eval("%@#{sql.gsub('@', '\@')}@", __FILE__, __LINE__) - end - # Instantiates objects for all attribute classes that needs more than one constructor parameter. This is done # by calling new on the column type or aggregation type (through composed_of) object with these parameters. # So having the pairs written_on(1) = "2004", written_on(2) = "6", written_on(3) = "24", will instantiate @@ -1828,27 +1917,20 @@ MSG end end - def object_from_yaml(string) - return string unless string.is_a?(String) && string =~ /^---/ - YAML::load(string) rescue string - end - def populate_with_current_scope_attributes if scope = self.class.send(:current_scoped_methods) create_with = scope.scope_for_create create_with.each { |att,value| - respond_to?(:"#{att}=") && send("#{att}=", value) + respond_to?("#{att}=") && send("#{att}=", value) } end end # Clear attributes and changed_attributes def clear_timestamp_attributes - %w(created_at created_on updated_at updated_on).each do |attribute_name| - if has_attribute?(attribute_name) - self[attribute_name] = nil - changed_attributes.delete(attribute_name) - end + all_timestamp_attributes_in_model.each do |attribute_name| + self[attribute_name] = nil + changed_attributes.delete(attribute_name) end end end diff --git a/activerecord/lib/active_record/callbacks.rb b/activerecord/lib/active_record/callbacks.rb index 47428cfd0f..ff4ce1b605 100644 --- a/activerecord/lib/active_record/callbacks.rb +++ b/activerecord/lib/active_record/callbacks.rb @@ -25,9 +25,13 @@ module ActiveRecord # Check out <tt>ActiveRecord::Transactions</tt> for more details about <tt>after_commit</tt> and # <tt>after_rollback</tt>. # - # That's a total of ten callbacks, which gives you immense power to react and prepare for each state in the + # Lastly an <tt>after_find</tt> and <tt>after_initialize</tt> callback is triggered for each object that + # is found and instantiated by a finder, with <tt>after_initialize</tt> being triggered after new objects + # are instantiated as well. + # + # That's a total of twelve callbacks, which gives you immense power to react and prepare for each state in the # Active Record life cycle. The sequence for calling <tt>Base#save</tt> for an existing record is similar, - # except that each <tt>_on_create</tt> callback is replaced by the corresponding <tt>_on_update</tt> callback. + # except that each <tt>_create</tt> callback is replaced by the corresponding <tt>_update</tt> callback. # # Examples: # class CreditCard < ActiveRecord::Base @@ -185,14 +189,6 @@ module ActiveRecord # 'puts "Evaluated after parents are destroyed"' # end # - # == The +after_find+ and +after_initialize+ exceptions - # - # Because +after_find+ and +after_initialize+ are called for each object found and instantiated by a finder, - # such as <tt>Base.find(:all)</tt>, we've had to implement a simple performance constraint (50% more speed - # on a simple test case). Unlike all the other callbacks, +after_find+ and +after_initialize+ will only be - # run if an explicit implementation is defined (<tt>def after_find</tt>). In that case, all of the - # callback types will be called. - # # == <tt>before_validation*</tt> returning statements # # If the returning value of a +before_validation+ callback can be evaluated to +false+, the process will be @@ -237,25 +233,25 @@ module ActiveRecord end def destroy #:nodoc: - _run_destroy_callbacks { super } + run_callbacks(:destroy) { super } end def touch(*) #:nodoc: - _run_touch_callbacks { super } + run_callbacks(:touch) { super } end private def create_or_update #:nodoc: - _run_save_callbacks { super } + run_callbacks(:save) { super } end def create #:nodoc: - _run_create_callbacks { super } + run_callbacks(:create) { super } end def update(*) #:nodoc: - _run_update_callbacks { super } + run_callbacks(:update) { super } end end end diff --git a/activerecord/lib/active_record/coders/yaml_column.rb b/activerecord/lib/active_record/coders/yaml_column.rb new file mode 100644 index 0000000000..fb59d9fb07 --- /dev/null +++ b/activerecord/lib/active_record/coders/yaml_column.rb @@ -0,0 +1,41 @@ +module ActiveRecord + # :stopdoc: + module Coders + class YAMLColumn + RESCUE_ERRORS = [ ArgumentError ] + + if defined?(Psych) && defined?(Psych::SyntaxError) + RESCUE_ERRORS << Psych::SyntaxError + end + + attr_accessor :object_class + + def initialize(object_class = Object) + @object_class = object_class + end + + def dump(obj) + YAML.dump obj + end + + def load(yaml) + return object_class.new if object_class != Object && yaml.nil? + return yaml unless yaml.is_a?(String) && yaml =~ /^---/ + begin + obj = YAML.load(yaml) + + unless obj.is_a?(object_class) || obj.nil? + raise SerializationTypeMismatch, + "Attribute was supposed to be a #{object_class}, but was a #{obj.class}" + end + obj ||= object_class.new if object_class != Object + + obj + rescue *RESCUE_ERRORS + yaml + end + end + end + end + # :startdoc +end 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 cffa2387de..4297c26413 100644 --- a/activerecord/lib/active_record/connection_adapters/abstract/connection_pool.rb +++ b/activerecord/lib/active_record/connection_adapters/abstract/connection_pool.rb @@ -57,7 +57,9 @@ 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_accessor :automatic_reconnect attr_reader :spec, :connections + attr_reader :columns, :columns_hash, :primary_keys, :tables # Creates a new ConnectionPool object. +spec+ is a ConnectionSpecification # object which describes database connection information (e.g. adapter, @@ -81,6 +83,63 @@ module ActiveRecord @connections = [] @checked_out = [] + @automatic_reconnect = true + @tables = {} + + @columns = Hash.new do |h, table_name| + h[table_name] = with_connection do |conn| + + # Fetch a list of columns + conn.columns(table_name, "#{table_name} Columns").tap do |columns| + + # set primary key information + columns.each do |column| + column.primary = column.name == primary_keys[table_name] + end + end + end + end + + @columns_hash = Hash.new do |h, table_name| + h[table_name] = Hash[columns[table_name].map { |col| + [col.name, col] + }] + end + + @primary_keys = Hash.new do |h, table_name| + h[table_name] = with_connection do |conn| + table_exists?(table_name) ? conn.primary_key(table_name) : 'id' + end + end + end + + # A cached lookup for table existence + def table_exists?(name) + return true if @tables.key? name + + with_connection do |conn| + conn.tables.each { |table| @tables[table] = true } + end + + @tables.key? name + end + + # Clears out internal caches: + # + # * columns + # * columns_hash + # * tables + def clear_cache! + @columns.clear + @columns_hash.clear + @tables.clear + end + + # Clear out internal caches for table with +table_name+ + def clear_table_cache!(table_name) + @columns.delete table_name + @columns_hash.delete table_name + @primary_keys.delete table_name end # Retrieve the connection associated with the current thread, or call @@ -212,7 +271,7 @@ module ActiveRecord # calling +checkout+ on this pool. def checkin(conn) @connection_mutex.synchronize do - conn.send(:_run_checkin_callbacks) do + conn.run_callbacks :checkin do @checked_out.delete conn @queue.signal end @@ -232,6 +291,8 @@ module ActiveRecord end def checkout_new_connection + raise ConnectionNotEstablished unless @automatic_reconnect + c = new_connection @connections << c checkout_and_verify(c) @@ -330,7 +391,7 @@ module ActiveRecord pool = @connection_pools[klass.name] return nil unless pool - @connection_pools.delete_if { |key, value| value == pool } + pool.automatic_reconnect = false pool.disconnect! pool.spec.config end diff --git a/activerecord/lib/active_record/connection_adapters/abstract/database_statements.rb b/activerecord/lib/active_record/connection_adapters/abstract/database_statements.rb index ee9a0af35c..5c1ce173c8 100644 --- a/activerecord/lib/active_record/connection_adapters/abstract/database_statements.rb +++ b/activerecord/lib/active_record/connection_adapters/abstract/database_statements.rb @@ -1,3 +1,5 @@ +require 'active_support/core_ext/module/deprecation' + module ActiveRecord module ConnectionAdapters # :nodoc: module DatabaseStatements @@ -229,6 +231,8 @@ module ActiveRecord # # This method *modifies* the +sql+ parameter. # + # This method is deprecated!! Stop using it! + # # ===== Examples # add_limit_offset!('SELECT * FROM suppliers', {:limit => 10, :offset => 50}) # generates @@ -243,6 +247,7 @@ module ActiveRecord end sql end + deprecate :add_limit_offset! def default_sequence_name(table, column) nil @@ -256,7 +261,15 @@ module ActiveRecord # Inserts the given fixture into the table. Overridden in adapters that require # something beyond a simple insert (eg. Oracle). def insert_fixture(fixture, table_name) - execute "INSERT INTO #{quote_table_name(table_name)} (#{fixture.key_list}) VALUES (#{fixture.value_list})", 'Fixture Insert' + columns = Hash[columns(table_name).map { |c| [c.name, c] }] + + key_list = [] + value_list = fixture.map do |name, value| + key_list << quote_column_name(name) + quote(value, columns[name]) + end + + execute "INSERT INTO #{quote_table_name(table_name)} (#{key_list.join(', ')}) VALUES (#{value_list.join(', ')})", 'Fixture Insert' end def empty_insert_statement_value @@ -271,6 +284,25 @@ module ActiveRecord "WHERE #{quoted_primary_key} IN (SELECT #{quoted_primary_key} FROM #{quoted_table_name} #{where_sql})" end + # Sanitizes the given LIMIT parameter in order to prevent SQL injection. + # + # The +limit+ may be anything that can evaluate to a string via #to_s. It + # should look like an integer, or a comma-delimited list of integers, or + # an Arel SQL literal. + # + # Returns Integer and Arel::Nodes::SqlLiteral limits as is. + # Returns the sanitized limit parameter, either as an integer, or as a + # string which contains a comma-delimited list of integers. + def sanitize_limit(limit) + if limit.is_a?(Integer) || limit.is_a?(Arel::Nodes::SqlLiteral) + limit + elsif limit.to_s =~ /,/ + Arel.sql limit.to_s.split(',').map{ |i| Integer(i) }.join(',') + else + Integer(limit) + end + end + protected # Returns an array of record hashes with the column names as keys and # column values as values. @@ -294,21 +326,6 @@ module ActiveRecord update_sql(sql, name) end - # Sanitizes the given LIMIT parameter in order to prevent SQL injection. - # - # +limit+ may be anything that can evaluate to a string via #to_s. It - # should look like an integer, or a comma-delimited list of integers. - # - # Returns the sanitized limit parameter, either as an integer, or as a - # string which contains a comma-delimited list of integers. - def sanitize_limit(limit) - if limit.to_s =~ /,/ - limit.to_s.split(',').map{ |i| i.to_i }.join(',') - else - limit.to_i - end - end - # Send a rollback message to all records after they have been rolled back. If rollback # is false, only rollback records since the last save point. def rollback_transaction_records(rollback) #:nodoc 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 d555308485..1db397f584 100644 --- a/activerecord/lib/active_record/connection_adapters/abstract/query_cache.rb +++ b/activerecord/lib/active_record/connection_adapters/abstract/query_cache.rb @@ -60,7 +60,7 @@ module ActiveRecord result = if @query_cache[sql].key?(binds) ActiveSupport::Notifications.instrument("sql.active_record", - :sql => sql, :name => "CACHE", :connection_id => self.object_id) + :sql => sql, :name => "CACHE", :connection_id => object_id) @query_cache[sql][binds] else @query_cache[sql][binds] = yield diff --git a/activerecord/lib/active_record/connection_adapters/abstract/quoting.rb b/activerecord/lib/active_record/connection_adapters/abstract/quoting.rb index a7a12faac2..7489e88eef 100644 --- a/activerecord/lib/active_record/connection_adapters/abstract/quoting.rb +++ b/activerecord/lib/active_record/connection_adapters/abstract/quoting.rb @@ -33,8 +33,9 @@ module ActiveRecord when BigDecimal then value.to_s('F') when Numeric then value.to_s when Date, Time then "'#{quoted_date(value)}'" + when Symbol then "'#{quote_string(value.to_s)}'" else - "'#{quote_string(value.to_s)}'" + "'#{quote_string(value.to_yaml)}'" end end diff --git a/activerecord/lib/active_record/connection_adapters/abstract/schema_definitions.rb b/activerecord/lib/active_record/connection_adapters/abstract/schema_definitions.rb index 60ccf9edf3..7ac48c6646 100644 --- a/activerecord/lib/active_record/connection_adapters/abstract/schema_definitions.rb +++ b/activerecord/lib/active_record/connection_adapters/abstract/schema_definitions.rb @@ -6,264 +6,6 @@ require 'bigdecimal/util' module ActiveRecord module ConnectionAdapters #:nodoc: - # An abstract definition of a column in a table. - class Column - TRUE_VALUES = [true, 1, '1', 't', 'T', 'true', 'TRUE'].to_set - FALSE_VALUES = [false, 0, '0', 'f', 'F', 'false', 'FALSE'].to_set - - module Format - ISO_DATE = /\A(\d{4})-(\d\d)-(\d\d)\z/ - ISO_DATETIME = /\A(\d{4})-(\d\d)-(\d\d) (\d\d):(\d\d):(\d\d)(\.\d+)?\z/ - end - - attr_reader :name, :default, :type, :limit, :null, :sql_type, :precision, :scale - attr_accessor :primary - - # Instantiates a new column in the table. - # - # +name+ is the column's name, such as <tt>supplier_id</tt> in <tt>supplier_id int(11)</tt>. - # +default+ is the type-casted default value, such as +new+ in <tt>sales_stage varchar(20) default 'new'</tt>. - # +sql_type+ is used to extract the column's length, if necessary. For example +60+ in - # <tt>company_name varchar(60)</tt>. - # It will be mapped to one of the standard Rails SQL types in the <tt>type</tt> attribute. - # +null+ determines if this column allows +NULL+ values. - def initialize(name, default, sql_type = nil, null = true) - @name, @sql_type, @null = name, sql_type, null - @limit, @precision, @scale = extract_limit(sql_type), extract_precision(sql_type), extract_scale(sql_type) - @type = simplified_type(sql_type) - @default = extract_default(default) - - @primary = nil - end - - # Returns +true+ if the column is either of type string or text. - def text? - type == :string || type == :text - end - - # Returns +true+ if the column is either of type integer, float or decimal. - def number? - type == :integer || type == :float || type == :decimal - end - - def has_default? - !default.nil? - end - - # Returns the Ruby class that corresponds to the abstract data type. - def klass - case type - when :integer then Fixnum - when :float then Float - when :decimal then BigDecimal - when :datetime then Time - when :date then Date - when :timestamp then Time - when :time then Time - when :text, :string then String - when :binary then String - when :boolean then Object - end - end - - # Casts value (which is a String) to an appropriate instance. - def type_cast(value) - return nil if value.nil? - case type - when :string then value - when :text then value - when :integer then value.to_i rescue value ? 1 : 0 - when :float then value.to_f - when :decimal then self.class.value_to_decimal(value) - when :datetime then self.class.string_to_time(value) - when :timestamp then self.class.string_to_time(value) - when :time then self.class.string_to_dummy_time(value) - when :date then self.class.string_to_date(value) - when :binary then self.class.binary_to_string(value) - when :boolean then self.class.value_to_boolean(value) - else value - end - end - - def type_cast_code(var_name) - case type - when :string then nil - when :text then nil - when :integer then "(#{var_name}.to_i rescue #{var_name} ? 1 : 0)" - when :float then "#{var_name}.to_f" - when :decimal then "#{self.class.name}.value_to_decimal(#{var_name})" - when :datetime then "#{self.class.name}.string_to_time(#{var_name})" - when :timestamp then "#{self.class.name}.string_to_time(#{var_name})" - when :time then "#{self.class.name}.string_to_dummy_time(#{var_name})" - when :date then "#{self.class.name}.string_to_date(#{var_name})" - when :binary then "#{self.class.name}.binary_to_string(#{var_name})" - when :boolean then "#{self.class.name}.value_to_boolean(#{var_name})" - else nil - end - end - - # Returns the human name of the column name. - # - # ===== Examples - # Column.new('sales_stage', ...).human_name # => 'Sales stage' - def human_name - Base.human_attribute_name(@name) - end - - def extract_default(default) - type_cast(default) - end - - # Used to convert from Strings to BLOBs - def string_to_binary(value) - self.class.string_to_binary(value) - end - - class << self - # Used to convert from Strings to BLOBs - def string_to_binary(value) - value - end - - # Used to convert from BLOBs to Strings - def binary_to_string(value) - value - end - - def string_to_date(string) - return string unless string.is_a?(String) - return nil if string.empty? - - fast_string_to_date(string) || fallback_string_to_date(string) - end - - def string_to_time(string) - return string unless string.is_a?(String) - return nil if string.empty? - - fast_string_to_time(string) || fallback_string_to_time(string) - end - - def string_to_dummy_time(string) - return string unless string.is_a?(String) - return nil if string.empty? - - string_to_time "2000-01-01 #{string}" - end - - # convert something to a boolean - def value_to_boolean(value) - if value.is_a?(String) && value.blank? - nil - else - TRUE_VALUES.include?(value) - end - end - - # convert something to a BigDecimal - def value_to_decimal(value) - # Using .class is faster than .is_a? and - # subclasses of BigDecimal will be handled - # in the else clause - if value.class == BigDecimal - value - elsif value.respond_to?(:to_d) - value.to_d - else - value.to_s.to_d - end - end - - protected - # '0.123456' -> 123456 - # '1.123456' -> 123456 - def microseconds(time) - ((time[:sec_fraction].to_f % 1) * 1_000_000).to_i - end - - def new_date(year, mon, mday) - if year && year != 0 - Date.new(year, mon, mday) rescue nil - end - end - - def new_time(year, mon, mday, hour, min, sec, microsec) - # Treat 0000-00-00 00:00:00 as nil. - return nil if year.nil? || year == 0 - - Time.time_with_datetime_fallback(Base.default_timezone, year, mon, mday, hour, min, sec, microsec) rescue nil - end - - def fast_string_to_date(string) - if string =~ Format::ISO_DATE - new_date $1.to_i, $2.to_i, $3.to_i - end - end - - # Doesn't handle time zones. - def fast_string_to_time(string) - if string =~ Format::ISO_DATETIME - microsec = ($7.to_f * 1_000_000).to_i - new_time $1.to_i, $2.to_i, $3.to_i, $4.to_i, $5.to_i, $6.to_i, microsec - end - end - - def fallback_string_to_date(string) - new_date(*::Date._parse(string, false).values_at(:year, :mon, :mday)) - end - - def fallback_string_to_time(string) - time_hash = Date._parse(string) - time_hash[:sec_fraction] = microseconds(time_hash) - - new_time(*time_hash.values_at(:year, :mon, :mday, :hour, :min, :sec, :sec_fraction)) - end - end - - private - def extract_limit(sql_type) - $1.to_i if sql_type =~ /\((.*)\)/ - end - - def extract_precision(sql_type) - $2.to_i if sql_type =~ /^(numeric|decimal|number)\((\d+)(,\d+)?\)/i - end - - def extract_scale(sql_type) - case sql_type - when /^(numeric|decimal|number)\((\d+)\)/i then 0 - when /^(numeric|decimal|number)\((\d+)(,(\d+))\)/i then $4.to_i - end - end - - def simplified_type(field_type) - case field_type - when /int/i - :integer - when /float|double/i - :float - when /decimal|numeric|number/i - extract_scale(field_type) == 0 ? :integer : :decimal - when /datetime/i - :datetime - when /timestamp/i - :timestamp - when /time/i - :time - when /date/i - :date - when /clob/i, /text/i - :text - when /blob/i, /binary/i - :binary - when /char/i, /string/i - :string - when /boolean/i - :boolean - end - end - end - class IndexDefinition < Struct.new(:table, :name, :unique, :columns, :lengths) #:nodoc: end 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 5b9c48bafa..3ec7dd02a4 100644 --- a/activerecord/lib/active_record/connection_adapters/abstract/schema_statements.rb +++ b/activerecord/lib/active_record/connection_adapters/abstract/schema_statements.rb @@ -176,6 +176,13 @@ module ActiveRecord # # Other column alterations here # end # + # The +options+ hash can include the following keys: + # [<tt>:bulk</tt>] + # Set this to true to make this a bulk alter query, such as + # ALTER TABLE `users` ADD COLUMN age INT(11), ADD COLUMN birthdate DATETIME ... + # + # Defaults to false. + # # ===== Examples # ====== Add a column # change_table(:suppliers) do |t| @@ -224,8 +231,14 @@ module ActiveRecord # # See also Table for details on # all of the various column transformation - def change_table(table_name) - yield Table.new(table_name, self) + def change_table(table_name, options = {}) + if supports_bulk_alter? && options[:bulk] + recorder = ActiveRecord::Migration::CommandRecorder.new(self) + yield Table.new(table_name, recorder) + bulk_change_table(table_name, recorder.commands) + else + yield Table.new(table_name, self) + end end # Renames a table. @@ -253,10 +266,7 @@ module ActiveRecord # remove_column(:suppliers, :qualification) # remove_columns(:suppliers, :qualification, :experience) def remove_column(table_name, *column_names) - raise ArgumentError.new("You must specify at least one column name. Example: remove_column(:people, :first_name)") if column_names.empty? - column_names.flatten.each do |column_name| - execute "ALTER TABLE #{quote_table_name(table_name)} DROP #{quote_column_name(column_name)}" - end + columns_for_remove(table_name, *column_names).each {|column_name| execute "ALTER TABLE #{quote_table_name(table_name)} DROP #{column_name}" } end alias :remove_columns :remove_column @@ -327,25 +337,8 @@ module ActiveRecord # # Note: SQLite doesn't support index length def add_index(table_name, column_name, options = {}) - column_names = Array.wrap(column_name) - index_name = index_name(table_name, :column => column_names) - - if Hash === options # legacy support, since this param was a string - index_type = options[:unique] ? "UNIQUE" : "" - index_name = options[:name].to_s if options.key?(:name) - else - index_type = options - end - - if index_name.length > index_name_length - raise ArgumentError, "Index name '#{index_name}' on table '#{table_name}' is too long; the limit is #{index_name_length} characters" - end - if index_name_exists?(table_name, index_name, false) - raise ArgumentError, "Index name '#{index_name}' on table '#{table_name}' already exists" - end - quoted_column_names = quoted_columns_for_index(column_names, options).join(", ") - - execute "CREATE #{index_type} INDEX #{quote_column_name(index_name)} ON #{quote_table_name(table_name)} (#{quoted_column_names})" + index_name, index_type, index_columns = add_index_options(table_name, column_name, options) + execute "CREATE #{index_type} INDEX #{quote_column_name(index_name)} ON #{quote_table_name(table_name)} (#{index_columns})" end # Remove the given index from the table. @@ -359,11 +352,7 @@ module ActiveRecord # Remove the index named by_branch_party in the accounts table. # remove_index :accounts, :name => :by_branch_party def remove_index(table_name, options = {}) - index_name = index_name(table_name, options) - unless index_name_exists?(table_name, index_name, true) - raise ArgumentError, "Index name '#{index_name}' on table '#{table_name}' does not exist" - end - remove_index!(table_name, index_name) + remove_index!(table_name, index_name_for_remove(table_name, options)) end def remove_index!(table_name, index_name) #:nodoc: @@ -469,7 +458,7 @@ module ActiveRecord end def type_to_sql(type, limit = nil, precision = nil, scale = nil) #:nodoc: - if native = native_database_types[type] + if native = native_database_types[type.to_sym] column_type_sql = (native.is_a?(Hash) ? native[:name] : native).dup if type == :decimal # ignore limit, use precision and scale @@ -537,6 +526,45 @@ module ActiveRecord options.include?(:default) && !(options[:null] == false && options[:default].nil?) end + def add_index_options(table_name, column_name, options = {}) + column_names = Array.wrap(column_name) + index_name = index_name(table_name, :column => column_names) + + if Hash === options # legacy support, since this param was a string + index_type = options[:unique] ? "UNIQUE" : "" + index_name = options[:name].to_s if options.key?(:name) + else + index_type = options + end + + if index_name.length > index_name_length + raise ArgumentError, "Index name '#{index_name}' on table '#{table_name}' is too long; the limit is #{index_name_length} characters" + end + if index_name_exists?(table_name, index_name, false) + raise ArgumentError, "Index name '#{index_name}' on table '#{table_name}' already exists" + end + index_columns = quoted_columns_for_index(column_names, options).join(", ") + + [index_name, index_type, index_columns] + end + + def index_name_for_remove(table_name, options = {}) + index_name = index_name(table_name, options) + + unless index_name_exists?(table_name, index_name, true) + raise ArgumentError, "Index name '#{index_name}' on table '#{table_name}' does not exist" + end + + index_name + end + + def columns_for_remove(table_name, *column_names) + column_names = column_names.flatten + + raise ArgumentError.new("You must specify at least one column name. Example: remove_column(:people, :first_name)") if column_names.blank? + column_names.map {|column_name| quote_column_name(column_name) } + end + private def table_definition TableDefinition.new(self) diff --git a/activerecord/lib/active_record/connection_adapters/abstract_adapter.rb b/activerecord/lib/active_record/connection_adapters/abstract_adapter.rb index 0282493219..0f44baa2fe 100644 --- a/activerecord/lib/active_record/connection_adapters/abstract_adapter.rb +++ b/activerecord/lib/active_record/connection_adapters/abstract_adapter.rb @@ -4,6 +4,7 @@ require 'bigdecimal/util' require 'active_support/core_ext/benchmark' # TODO: Autoload these files +require 'active_record/connection_adapters/column' require 'active_record/connection_adapters/abstract/schema_definitions' require 'active_record/connection_adapters/abstract/schema_statements' require 'active_record/connection_adapters/abstract/database_statements' @@ -77,8 +78,12 @@ module ActiveRecord false end - # Does this adapter support savepoints? PostgreSQL and MySQL do, SQLite - # does not. + def supports_bulk_alter? + false + end + + # Does this adapter support savepoints? PostgreSQL and MySQL do, + # SQLite < 3.6.8 does not. def supports_savepoints? false end @@ -204,11 +209,13 @@ module ActiveRecord protected - def log(sql, name = "SQL") - @instrumenter.instrument("sql.active_record", - :sql => sql, :name => name, :connection_id => object_id) do - yield - end + def log(sql, name = "SQL", binds = []) + @instrumenter.instrument( + "sql.active_record", + :sql => sql, + :name => name, + :connection_id => object_id, + :binds => binds) { yield } rescue Exception => e message = "#{e.class.name}: #{e.message}: #{sql}" @logger.debug message if @logger diff --git a/activerecord/lib/active_record/connection_adapters/column.rb b/activerecord/lib/active_record/connection_adapters/column.rb new file mode 100644 index 0000000000..4e3d8a096f --- /dev/null +++ b/activerecord/lib/active_record/connection_adapters/column.rb @@ -0,0 +1,268 @@ +module ActiveRecord + # :stopdoc: + module ConnectionAdapters + # An abstract definition of a column in a table. + class Column + TRUE_VALUES = [true, 1, '1', 't', 'T', 'true', 'TRUE'].to_set + FALSE_VALUES = [false, 0, '0', 'f', 'F', 'false', 'FALSE'].to_set + + module Format + ISO_DATE = /\A(\d{4})-(\d\d)-(\d\d)\z/ + ISO_DATETIME = /\A(\d{4})-(\d\d)-(\d\d) (\d\d):(\d\d):(\d\d)(\.\d+)?\z/ + end + + attr_reader :name, :default, :type, :limit, :null, :sql_type, :precision, :scale + attr_accessor :primary, :coder + + alias :encoded? :coder + + # Instantiates a new column in the table. + # + # +name+ is the column's name, such as <tt>supplier_id</tt> in <tt>supplier_id int(11)</tt>. + # +default+ is the type-casted default value, such as +new+ in <tt>sales_stage varchar(20) default 'new'</tt>. + # +sql_type+ is used to extract the column's length, if necessary. For example +60+ in + # <tt>company_name varchar(60)</tt>. + # It will be mapped to one of the standard Rails SQL types in the <tt>type</tt> attribute. + # +null+ determines if this column allows +NULL+ values. + def initialize(name, default, sql_type = nil, null = true) + @name = name + @sql_type = sql_type + @null = null + @limit = extract_limit(sql_type) + @precision = extract_precision(sql_type) + @scale = extract_scale(sql_type) + @type = simplified_type(sql_type) + @default = extract_default(default) + @primary = nil + @coder = nil + end + + # Returns +true+ if the column is either of type string or text. + def text? + type == :string || type == :text + end + + # Returns +true+ if the column is either of type integer, float or decimal. + def number? + type == :integer || type == :float || type == :decimal + end + + def has_default? + !default.nil? + end + + # Returns the Ruby class that corresponds to the abstract data type. + def klass + case type + when :integer then Fixnum + when :float then Float + when :decimal then BigDecimal + when :datetime, :timestamp, :time then Time + when :date then Date + when :text, :string, :binary then String + when :boolean then Object + end + end + + # Casts value (which is a String) to an appropriate instance. + def type_cast(value) + return nil if value.nil? + return coder.load(value) if encoded? + + klass = self.class + + case type + when :string, :text then value + when :integer then value.to_i rescue value ? 1 : 0 + when :float then value.to_f + when :decimal then klass.value_to_decimal(value) + when :datetime, :timestamp then klass.string_to_time(value) + when :time then klass.string_to_dummy_time(value) + when :date then klass.string_to_date(value) + when :binary then klass.binary_to_string(value) + when :boolean then klass.value_to_boolean(value) + else value + end + end + + def type_cast_code(var_name) + klass = self.class.name + + case type + when :string, :text then var_name + when :integer then "(#{var_name}.to_i rescue #{var_name} ? 1 : 0)" + when :float then "#{var_name}.to_f" + when :decimal then "#{klass}.value_to_decimal(#{var_name})" + when :datetime, :timestamp then "#{klass}.string_to_time(#{var_name})" + when :time then "#{klass}.string_to_dummy_time(#{var_name})" + when :date then "#{klass}.string_to_date(#{var_name})" + when :binary then "#{klass}.binary_to_string(#{var_name})" + when :boolean then "#{klass}.value_to_boolean(#{var_name})" + else var_name + end + end + + # Returns the human name of the column name. + # + # ===== Examples + # Column.new('sales_stage', ...).human_name # => 'Sales stage' + def human_name + Base.human_attribute_name(@name) + end + + def extract_default(default) + type_cast(default) + end + + # Used to convert from Strings to BLOBs + def string_to_binary(value) + self.class.string_to_binary(value) + end + + class << self + # Used to convert from Strings to BLOBs + def string_to_binary(value) + value + end + + # Used to convert from BLOBs to Strings + def binary_to_string(value) + value + end + + def string_to_date(string) + return string unless string.is_a?(String) + return nil if string.empty? + + fast_string_to_date(string) || fallback_string_to_date(string) + end + + def string_to_time(string) + return string unless string.is_a?(String) + return nil if string.empty? + + fast_string_to_time(string) || fallback_string_to_time(string) + end + + def string_to_dummy_time(string) + return string unless string.is_a?(String) + return nil if string.empty? + + string_to_time "2000-01-01 #{string}" + end + + # convert something to a boolean + def value_to_boolean(value) + if value.is_a?(String) && value.blank? + nil + else + TRUE_VALUES.include?(value) + end + end + + # convert something to a BigDecimal + def value_to_decimal(value) + # Using .class is faster than .is_a? and + # subclasses of BigDecimal will be handled + # in the else clause + if value.class == BigDecimal + value + elsif value.respond_to?(:to_d) + value.to_d + else + value.to_s.to_d + end + end + + protected + # '0.123456' -> 123456 + # '1.123456' -> 123456 + def microseconds(time) + ((time[:sec_fraction].to_f % 1) * 1_000_000).to_i + end + + def new_date(year, mon, mday) + if year && year != 0 + Date.new(year, mon, mday) rescue nil + end + end + + def new_time(year, mon, mday, hour, min, sec, microsec) + # Treat 0000-00-00 00:00:00 as nil. + return nil if year.nil? || year == 0 + + Time.time_with_datetime_fallback(Base.default_timezone, year, mon, mday, hour, min, sec, microsec) rescue nil + end + + def fast_string_to_date(string) + if string =~ Format::ISO_DATE + new_date $1.to_i, $2.to_i, $3.to_i + end + end + + # Doesn't handle time zones. + def fast_string_to_time(string) + if string =~ Format::ISO_DATETIME + microsec = ($7.to_f * 1_000_000).to_i + new_time $1.to_i, $2.to_i, $3.to_i, $4.to_i, $5.to_i, $6.to_i, microsec + end + end + + def fallback_string_to_date(string) + new_date(*::Date._parse(string, false).values_at(:year, :mon, :mday)) + end + + def fallback_string_to_time(string) + time_hash = Date._parse(string) + time_hash[:sec_fraction] = microseconds(time_hash) + + new_time(*time_hash.values_at(:year, :mon, :mday, :hour, :min, :sec, :sec_fraction)) + end + end + + private + def extract_limit(sql_type) + $1.to_i if sql_type =~ /\((.*)\)/ + end + + def extract_precision(sql_type) + $2.to_i if sql_type =~ /^(numeric|decimal|number)\((\d+)(,\d+)?\)/i + end + + def extract_scale(sql_type) + case sql_type + when /^(numeric|decimal|number)\((\d+)\)/i then 0 + when /^(numeric|decimal|number)\((\d+)(,(\d+))\)/i then $4.to_i + end + end + + def simplified_type(field_type) + case field_type + when /int/i + :integer + when /float|double/i + :float + when /decimal|numeric|number/i + extract_scale(field_type) == 0 ? :integer : :decimal + when /datetime/i + :datetime + when /timestamp/i + :timestamp + when /time/i + :time + when /date/i + :date + when /clob/i, /text/i + :text + when /blob/i, /binary/i + :binary + when /char/i, /string/i + :string + when /boolean/i + :boolean + end + end + end + end + # :startdoc: +end diff --git a/activerecord/lib/active_record/connection_adapters/mysql2_adapter.rb b/activerecord/lib/active_record/connection_adapters/mysql2_adapter.rb new file mode 100644 index 0000000000..7bad511c64 --- /dev/null +++ b/activerecord/lib/active_record/connection_adapters/mysql2_adapter.rb @@ -0,0 +1,610 @@ +# encoding: utf-8 + +require 'mysql2' + +module ActiveRecord + class Base + def self.mysql2_connection(config) + config[:username] = 'root' if config[:username].nil? + + if Mysql2::Client.const_defined? :FOUND_ROWS + config[:flags] = Mysql2::Client::FOUND_ROWS + end + + client = Mysql2::Client.new(config.symbolize_keys) + options = [config[:host], config[:username], config[:password], config[:database], config[:port], config[:socket], 0] + ConnectionAdapters::Mysql2Adapter.new(client, logger, options, config) + end + end + + module ConnectionAdapters + class Mysql2IndexDefinition < Struct.new(:table, :name, :unique, :columns, :lengths) #:nodoc: + end + + class Mysql2Column < Column + BOOL = "tinyint(1)" + def extract_default(default) + if sql_type =~ /blob/i || type == :text + if default.blank? + return null ? nil : '' + else + raise ArgumentError, "#{type} columns cannot have a default value: #{default.inspect}" + end + elsif missing_default_forged_as_empty_string?(default) + nil + else + super + end + end + + def has_default? + return false if sql_type =~ /blob/i || type == :text #mysql forbids defaults on blob and text columns + super + end + + private + def simplified_type(field_type) + return :boolean if Mysql2Adapter.emulate_booleans && field_type.downcase.index(BOOL) + + case field_type + when /enum/i, /set/i then :string + when /year/i then :integer + when /bit/i then :binary + else + super + end + end + + def extract_limit(sql_type) + case sql_type + when /blob|text/i + case sql_type + when /tiny/i + 255 + when /medium/i + 16777215 + when /long/i + 2147483647 # mysql only allows 2^31-1, not 2^32-1, somewhat inconsistently with the tiny/medium/normal cases + else + super # we could return 65535 here, but we leave it undecorated by default + end + when /^bigint/i; 8 + when /^int/i; 4 + when /^mediumint/i; 3 + when /^smallint/i; 2 + when /^tinyint/i; 1 + else + super + end + end + + # MySQL misreports NOT NULL column default when none is given. + # We can't detect this for columns which may have a legitimate '' + # default (string) but we can for others (integer, datetime, boolean, + # and the rest). + # + # Test whether the column has default '', is not null, and is not + # a type allowing default ''. + def missing_default_forged_as_empty_string?(default) + type != :string && !null && default == '' + end + end + + class Mysql2Adapter < AbstractAdapter + cattr_accessor :emulate_booleans + self.emulate_booleans = true + + ADAPTER_NAME = 'Mysql2' + PRIMARY = "PRIMARY" + + LOST_CONNECTION_ERROR_MESSAGES = [ + "Server shutdown in progress", + "Broken pipe", + "Lost connection to MySQL server during query", + "MySQL server has gone away" ] + + QUOTED_TRUE, QUOTED_FALSE = '1', '0' + + NATIVE_DATABASE_TYPES = { + :primary_key => "int(11) DEFAULT NULL auto_increment PRIMARY KEY", + :string => { :name => "varchar", :limit => 255 }, + :text => { :name => "text" }, + :integer => { :name => "int", :limit => 4 }, + :float => { :name => "float" }, + :decimal => { :name => "decimal" }, + :datetime => { :name => "datetime" }, + :timestamp => { :name => "datetime" }, + :time => { :name => "time" }, + :date => { :name => "date" }, + :binary => { :name => "blob" }, + :boolean => { :name => "tinyint", :limit => 1 } + } + + def initialize(connection, logger, connection_options, config) + super(connection, logger) + @connection_options, @config = connection_options, config + @quoted_column_names, @quoted_table_names = {}, {} + configure_connection + end + + def adapter_name + ADAPTER_NAME + end + + def supports_migrations? + true + end + + def supports_primary_key? + true + end + + def supports_savepoints? + true + end + + def native_database_types + NATIVE_DATABASE_TYPES + end + + # QUOTING ================================================== + + def quote(value, column = nil) + if value.kind_of?(String) && column && column.type == :binary && column.class.respond_to?(:string_to_binary) + s = column.class.string_to_binary(value).unpack("H*")[0] + "x'#{s}'" + elsif value.kind_of?(BigDecimal) + value.to_s("F") + else + super + end + end + + def quote_column_name(name) #:nodoc: + @quoted_column_names[name] ||= "`#{name}`" + end + + def quote_table_name(name) #:nodoc: + @quoted_table_names[name] ||= quote_column_name(name).gsub('.', '`.`') + end + + def quote_string(string) + @connection.escape(string) + end + + def quoted_true + QUOTED_TRUE + end + + def quoted_false + QUOTED_FALSE + end + + # REFERENTIAL INTEGRITY ==================================== + + def disable_referential_integrity(&block) #:nodoc: + old = select_value("SELECT @@FOREIGN_KEY_CHECKS") + + begin + update("SET FOREIGN_KEY_CHECKS = 0") + yield + ensure + update("SET FOREIGN_KEY_CHECKS = #{old}") + end + end + + # CONNECTION MANAGEMENT ==================================== + + def active? + return false unless @connection + @connection.ping + end + + def reconnect! + disconnect! + connect + end + + # this is set to true in 2.3, but we don't want it to be + def requires_reloading? + false + end + + def disconnect! + unless @connection.nil? + @connection.close + @connection = nil + end + end + + def reset! + disconnect! + connect + end + + # DATABASE STATEMENTS ====================================== + + # FIXME: re-enable the following once a "better" query_cache solution is in core + # + # The overrides below perform much better than the originals in AbstractAdapter + # because we're able to take advantage of mysql2's lazy-loading capabilities + # + # # Returns a record hash with the column names as keys and column values + # # as values. + # def select_one(sql, name = nil) + # result = execute(sql, name) + # result.each(:as => :hash) do |r| + # return r + # end + # end + # + # # Returns a single value from a record + # def select_value(sql, name = nil) + # result = execute(sql, name) + # if first = result.first + # first.first + # end + # end + # + # # Returns an array of the values of the first column in a select: + # # select_values("SELECT id FROM companies LIMIT 3") => [1,2,3] + # def select_values(sql, name = nil) + # execute(sql, name).map { |row| row.first } + # end + + # Returns an array of arrays containing the field values. + # Order is the same as that returned by +columns+. + def select_rows(sql, name = nil) + execute(sql, name).to_a + end + + # Executes the SQL statement in the context of this connection. + def execute(sql, name = nil) + # make sure we carry over any changes to ActiveRecord::Base.default_timezone that have been + # made since we established the connection + @connection.query_options[:database_timezone] = ActiveRecord::Base.default_timezone + if name == :skip_logging + @connection.query(sql) + else + log(sql, name) { @connection.query(sql) } + end + rescue ActiveRecord::StatementInvalid => exception + if exception.message.split(":").first =~ /Packets out of order/ + raise ActiveRecord::StatementInvalid, "'Packets out of order' error was received from the database. Please update your mysql bindings (gem install mysql) and read http://dev.mysql.com/doc/mysql/en/password-hashing.html for more information. If you're on Windows, use the Instant Rails installer to get the updated mysql bindings." + else + raise + end + end + + def insert_sql(sql, name = nil, pk = nil, id_value = nil, sequence_name = nil) + super + id_value || @connection.last_id + end + alias :create :insert_sql + + def update_sql(sql, name = nil) + super + @connection.affected_rows + end + + def begin_db_transaction + execute "BEGIN" + rescue Exception + # Transactions aren't supported + end + + def commit_db_transaction + execute "COMMIT" + rescue Exception + # Transactions aren't supported + end + + def rollback_db_transaction + execute "ROLLBACK" + rescue Exception + # Transactions aren't supported + end + + def create_savepoint + execute("SAVEPOINT #{current_savepoint_name}") + end + + def rollback_to_savepoint + execute("ROLLBACK TO SAVEPOINT #{current_savepoint_name}") + end + + def release_savepoint + execute("RELEASE SAVEPOINT #{current_savepoint_name}") + end + + def add_limit_offset!(sql, options) + limit, offset = options[:limit], options[:offset] + if limit && offset + sql << " LIMIT #{offset.to_i}, #{sanitize_limit(limit)}" + elsif limit + sql << " LIMIT #{sanitize_limit(limit)}" + elsif offset + sql << " OFFSET #{offset.to_i}" + end + sql + end + deprecate :add_limit_offset! + + # SCHEMA STATEMENTS ======================================== + + def structure_dump + if supports_views? + sql = "SHOW FULL TABLES WHERE Table_type = 'BASE TABLE'" + else + sql = "SHOW TABLES" + end + + select_all(sql).inject("") do |structure, table| + table.delete('Table_type') + structure += select_one("SHOW CREATE TABLE #{quote_table_name(table.to_a.first.last)}")["Create Table"] + ";\n\n" + end + end + + def recreate_database(name, options = {}) + drop_database(name) + create_database(name, options) + end + + # Create a new MySQL database with optional <tt>:charset</tt> and <tt>:collation</tt>. + # Charset defaults to utf8. + # + # Example: + # create_database 'charset_test', :charset => 'latin1', :collation => 'latin1_bin' + # create_database 'matt_development' + # create_database 'matt_development', :charset => :big5 + def create_database(name, options = {}) + if options[:collation] + execute "CREATE DATABASE `#{name}` DEFAULT CHARACTER SET `#{options[:charset] || 'utf8'}` COLLATE `#{options[:collation]}`" + else + execute "CREATE DATABASE `#{name}` DEFAULT CHARACTER SET `#{options[:charset] || 'utf8'}`" + end + end + + def drop_database(name) #:nodoc: + execute "DROP DATABASE IF EXISTS `#{name}`" + end + + def current_database + select_value 'SELECT DATABASE() as db' + end + + # Returns the database character set. + def charset + show_variable 'character_set_database' + end + + # Returns the database collation strategy. + def collation + show_variable 'collation_database' + end + + def tables(name = nil) + tables = [] + execute("SHOW TABLES", name).each do |field| + tables << field.first + end + tables + end + + def drop_table(table_name, options = {}) + super(table_name, options) + end + + def indexes(table_name, name = nil) + indexes = [] + current_index = nil + result = execute("SHOW KEYS FROM #{quote_table_name(table_name)}", name) + result.each(:symbolize_keys => true, :as => :hash) do |row| + if current_index != row[:Key_name] + next if row[:Key_name] == PRIMARY # skip the primary key + current_index = row[:Key_name] + indexes << Mysql2IndexDefinition.new(row[:Table], row[:Key_name], row[:Non_unique] == 0, [], []) + end + + indexes.last.columns << row[:Column_name] + indexes.last.lengths << row[:Sub_part] + end + indexes + end + + def columns(table_name, name = nil) + sql = "SHOW FIELDS FROM #{quote_table_name(table_name)}" + columns = [] + result = execute(sql) + result.each(:symbolize_keys => true, :as => :hash) { |field| + columns << Mysql2Column.new(field[:Field], field[:Default], field[:Type], field[:Null] == "YES") + } + columns + end + + def create_table(table_name, options = {}) + super(table_name, options.reverse_merge(:options => "ENGINE=InnoDB")) + end + + def rename_table(table_name, new_name) + execute "RENAME TABLE #{quote_table_name(table_name)} TO #{quote_table_name(new_name)}" + end + + def add_column(table_name, column_name, type, options = {}) + add_column_sql = "ALTER TABLE #{quote_table_name(table_name)} ADD #{quote_column_name(column_name)} #{type_to_sql(type, options[:limit], options[:precision], options[:scale])}" + add_column_options!(add_column_sql, options) + add_column_position!(add_column_sql, options) + execute(add_column_sql) + end + + def change_column_default(table_name, column_name, default) + column = column_for(table_name, column_name) + change_column table_name, column_name, column.sql_type, :default => default + end + + def change_column_null(table_name, column_name, null, default = nil) + column = column_for(table_name, column_name) + + unless null || default.nil? + execute("UPDATE #{quote_table_name(table_name)} SET #{quote_column_name(column_name)}=#{quote(default)} WHERE #{quote_column_name(column_name)} IS NULL") + end + + change_column table_name, column_name, column.sql_type, :null => null + end + + def change_column(table_name, column_name, type, options = {}) + column = column_for(table_name, column_name) + + unless options_include_default?(options) + options[:default] = column.default + end + + unless options.has_key?(:null) + options[:null] = column.null + end + + change_column_sql = "ALTER TABLE #{quote_table_name(table_name)} CHANGE #{quote_column_name(column_name)} #{quote_column_name(column_name)} #{type_to_sql(type, options[:limit], options[:precision], options[:scale])}" + add_column_options!(change_column_sql, options) + add_column_position!(change_column_sql, options) + execute(change_column_sql) + end + + def rename_column(table_name, column_name, new_column_name) + options = {} + if column = columns(table_name).find { |c| c.name == column_name.to_s } + options[:default] = column.default + options[:null] = column.null + else + raise ActiveRecordError, "No such column: #{table_name}.#{column_name}" + end + current_type = select_one("SHOW COLUMNS FROM #{quote_table_name(table_name)} LIKE '#{column_name}'")["Type"] + rename_column_sql = "ALTER TABLE #{quote_table_name(table_name)} CHANGE #{quote_column_name(column_name)} #{quote_column_name(new_column_name)} #{current_type}" + add_column_options!(rename_column_sql, options) + execute(rename_column_sql) + end + + # Maps logical Rails types to MySQL-specific data types. + def type_to_sql(type, limit = nil, precision = nil, scale = nil) + return super unless type.to_s == 'integer' + + case limit + when 1; 'tinyint' + when 2; 'smallint' + when 3; 'mediumint' + when nil, 4, 11; 'int(11)' # compatibility with MySQL default + when 5..8; 'bigint' + else raise(ActiveRecordError, "No integer type has byte size #{limit}") + end + end + + def add_column_position!(sql, options) + if options[:first] + sql << " FIRST" + elsif options[:after] + sql << " AFTER #{quote_column_name(options[:after])}" + end + end + + def show_variable(name) + variables = select_all("SHOW VARIABLES LIKE '#{name}'") + variables.first['Value'] unless variables.empty? + end + + def pk_and_sequence_for(table) + keys = [] + result = execute("describe #{quote_table_name(table)}") + result.each(:symbolize_keys => true, :as => :hash) do |row| + keys << row[:Field] if row[:Key] == "PRI" + end + keys.length == 1 ? [keys.first, nil] : nil + end + + # Returns just a table's primary key + def primary_key(table) + pk_and_sequence = pk_and_sequence_for(table) + pk_and_sequence && pk_and_sequence.first + end + + def case_sensitive_equality_operator + "= BINARY" + end + + def limited_update_conditions(where_sql, quoted_table_name, quoted_primary_key) + where_sql + end + + protected + def quoted_columns_for_index(column_names, options = {}) + length = options[:length] if options.is_a?(Hash) + + quoted_column_names = case length + when Hash + column_names.map {|name| length[name] ? "#{quote_column_name(name)}(#{length[name]})" : quote_column_name(name) } + when Fixnum + column_names.map {|name| "#{quote_column_name(name)}(#{length})"} + else + column_names.map {|name| quote_column_name(name) } + end + end + + def translate_exception(exception, message) + return super unless exception.respond_to?(:error_number) + + case exception.error_number + when 1062 + RecordNotUnique.new(message, exception) + when 1452 + InvalidForeignKey.new(message, exception) + else + super + end + end + + private + def connect + @connection = Mysql2::Client.new(@config) + configure_connection + end + + def configure_connection + @connection.query_options.merge!(:as => :array) + + # By default, MySQL 'where id is null' selects the last inserted id. + # Turn this off. http://dev.rubyonrails.org/ticket/6778 + variable_assignments = ['SQL_AUTO_IS_NULL=0'] + encoding = @config[:encoding] + + # make sure we set the encoding + variable_assignments << "NAMES '#{encoding}'" if encoding + + # increase timeout so mysql server doesn't disconnect us + wait_timeout = @config[:wait_timeout] + wait_timeout = 2592000 unless wait_timeout.is_a?(Fixnum) + variable_assignments << "@@wait_timeout = #{wait_timeout}" + + execute("SET #{variable_assignments.join(', ')}", :skip_logging) + end + + # Returns an array of record hashes with the column names as keys and + # column values as values. + def select(sql, name = nil) + execute(sql, name).each(:as => :hash) + end + + def supports_views? + version[0] >= 5 + end + + def version + @version ||= @connection.info[:version].scan(/^(\d+)\.(\d+)\.(\d+)/).flatten.map { |v| v.to_i } + end + + def column_for(table_name, column_name) + unless column = columns(table_name).find { |c| c.name == column_name.to_s } + raise "No such column: #{table_name}.#{column_name}" + end + column + 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 ce2352486b..368c5b2023 100644 --- a/activerecord/lib/active_record/connection_adapters/mysql_adapter.rb +++ b/activerecord/lib/active_record/connection_adapters/mysql_adapter.rb @@ -203,6 +203,10 @@ module ActiveRecord ADAPTER_NAME end + def supports_bulk_alter? #:nodoc: + true + end + # Returns +true+ when the connection adapter supports prepared statement # caching, otherwise returns +false+ def supports_statement_cache? @@ -327,7 +331,7 @@ module ActiveRecord end def exec_query(sql, name = 'SQL', binds = []) - log(sql, name) do + log(sql, name, binds) do result = nil cache = {} @@ -364,9 +368,14 @@ module ActiveRecord # statement API. For those queries, we need to use this method. :'( log(sql, name) do result = @connection.query(sql) - cols = result.fetch_fields.map { |field| field.name } - rows = result.to_a - result.free + cols = [] + rows = [] + + if result + cols = result.fetch_fields.map { |field| field.name } + rows = result.to_a + result.free + end ActiveRecord::Result.new(cols, rows) end end @@ -400,7 +409,7 @@ module ActiveRecord def begin_db_transaction #:nodoc: exec_without_stmt "BEGIN" - rescue Exception + rescue Mysql::Error # Transactions aren't supported end @@ -439,6 +448,7 @@ module ActiveRecord end sql end + deprecate :add_limit_offset! # SCHEMA STATEMENTS ======================================== @@ -527,7 +537,7 @@ module ActiveRecord def columns(table_name, name = nil)#:nodoc: sql = "SHOW FIELDS FROM #{quote_table_name(table_name)}" columns = [] - result = execute(sql, :skip_logging) + result = execute(sql) result.each { |field| columns << MysqlColumn.new(field[0], field[4], field[1], field[2] == "YES") } result.free columns @@ -541,11 +551,23 @@ module ActiveRecord execute "RENAME TABLE #{quote_table_name(table_name)} TO #{quote_table_name(new_name)}" end + def bulk_change_table(table_name, operations) #:nodoc: + sqls = operations.map do |command, args| + table, arguments = args.shift, args + method = :"#{command}_sql" + + if respond_to?(method) + send(method, table, *arguments) + else + raise "Unknown method called : #{method}(#{arguments.inspect})" + end + end.flatten.join(", ") + + execute("ALTER TABLE #{quote_table_name(table_name)} #{sqls}") + end + def add_column(table_name, column_name, type, options = {}) - add_column_sql = "ALTER TABLE #{quote_table_name(table_name)} ADD #{quote_column_name(column_name)} #{type_to_sql(type, options[:limit], options[:precision], options[:scale])}" - add_column_options!(add_column_sql, options) - add_column_position!(add_column_sql, options) - execute(add_column_sql) + execute("ALTER TABLE #{quote_table_name(table_name)} #{add_column_sql(table_name, column_name, type, options)}") end def change_column_default(table_name, column_name, default) #:nodoc: @@ -564,34 +586,11 @@ module ActiveRecord end def change_column(table_name, column_name, type, options = {}) #:nodoc: - column = column_for(table_name, column_name) - - unless options_include_default?(options) - options[:default] = column.default - end - - unless options.has_key?(:null) - options[:null] = column.null - end - - change_column_sql = "ALTER TABLE #{quote_table_name(table_name)} CHANGE #{quote_column_name(column_name)} #{quote_column_name(column_name)} #{type_to_sql(type, options[:limit], options[:precision], options[:scale])}" - add_column_options!(change_column_sql, options) - add_column_position!(change_column_sql, options) - execute(change_column_sql) + execute("ALTER TABLE #{quote_table_name(table_name)} #{change_column_sql(table_name, column_name, type, options)}") end def rename_column(table_name, column_name, new_column_name) #:nodoc: - options = {} - if column = columns(table_name).find { |c| c.name == column_name.to_s } - options[:default] = column.default - options[:null] = column.null - else - raise ActiveRecordError, "No such column: #{table_name}.#{column_name}" - end - current_type = select_one("SHOW COLUMNS FROM #{quote_table_name(table_name)} LIKE '#{column_name}'")["Type"] - rename_column_sql = "ALTER TABLE #{quote_table_name(table_name)} CHANGE #{quote_column_name(column_name)} #{quote_column_name(new_column_name)} #{current_type}" - add_column_options!(rename_column_sql, options) - execute(rename_column_sql) + execute("ALTER TABLE #{quote_table_name(table_name)} #{rename_column_sql(table_name, column_name, new_column_name)}") end # Maps logical Rails types to MySQL-specific data types. @@ -674,6 +673,69 @@ module ActiveRecord end end + def add_column_sql(table_name, column_name, type, options = {}) + add_column_sql = "ADD #{quote_column_name(column_name)} #{type_to_sql(type, options[:limit], options[:precision], options[:scale])}" + add_column_options!(add_column_sql, options) + add_column_position!(add_column_sql, options) + add_column_sql + end + + def remove_column_sql(table_name, *column_names) + columns_for_remove(table_name, *column_names).map {|column_name| "DROP #{column_name}" } + end + alias :remove_columns_sql :remove_column + + def change_column_sql(table_name, column_name, type, options = {}) + column = column_for(table_name, column_name) + + unless options_include_default?(options) + options[:default] = column.default + end + + unless options.has_key?(:null) + options[:null] = column.null + end + + change_column_sql = "CHANGE #{quote_column_name(column_name)} #{quote_column_name(column_name)} #{type_to_sql(type, options[:limit], options[:precision], options[:scale])}" + add_column_options!(change_column_sql, options) + add_column_position!(change_column_sql, options) + change_column_sql + end + + def rename_column_sql(table_name, column_name, new_column_name) + options = {} + + if column = columns(table_name).find { |c| c.name == column_name.to_s } + options[:default] = column.default + options[:null] = column.null + else + raise ActiveRecordError, "No such column: #{table_name}.#{column_name}" + end + + current_type = select_one("SHOW COLUMNS FROM #{quote_table_name(table_name)} LIKE '#{column_name}'")["Type"] + rename_column_sql = "CHANGE #{quote_column_name(column_name)} #{quote_column_name(new_column_name)} #{current_type}" + add_column_options!(rename_column_sql, options) + rename_column_sql + end + + def add_index_sql(table_name, column_name, options = {}) + index_name, index_type, index_columns = add_index_options(table_name, column_name, options) + "ADD #{index_type} INDEX #{index_name} (#{index_columns})" + end + + def remove_index_sql(table_name, options = {}) + index_name = index_name_for_remove(table_name, options) + "DROP INDEX #{index_name}" + end + + def add_timestamps_sql(table_name) + [add_column_sql(table_name, :created_at, :datetime), add_column_sql(table_name, :updated_at, :datetime)] + end + + def remove_timestamps_sql(table_name) + [remove_column_sql(table_name, :updated_at), remove_column_sql(table_name, :created_at)] + end + private def connect encoding = @config[:encoding] diff --git a/activerecord/lib/active_record/connection_adapters/postgresql_adapter.rb b/activerecord/lib/active_record/connection_adapters/postgresql_adapter.rb index a4b1aa7154..20be4a22ea 100644 --- a/activerecord/lib/active_record/connection_adapters/postgresql_adapter.rb +++ b/activerecord/lib/active_record/connection_adapters/postgresql_adapter.rb @@ -532,7 +532,7 @@ module ActiveRecord def exec_query(sql, name = 'SQL', binds = []) return exec_no_cache(sql, name) if binds.empty? - log(sql, name) do + log(sql, name, binds) do unless @statements.key? sql nextkey = "a#{@statements.length + 1}" @connection.prepare nextkey, sql @@ -786,7 +786,7 @@ module ActiveRecord def pk_and_sequence_for(table) #:nodoc: # First try looking for a sequence with a dependency on the # given table's primary key. - result = query(<<-end_sql, 'PK and serial sequence')[0] + result = exec_query(<<-end_sql, 'PK and serial sequence').rows.first SELECT attr.attname, seq.relname FROM pg_class seq, pg_attribute attr, @@ -1071,7 +1071,7 @@ module ActiveRecord # - format_type includes the column size constraint, e.g. varchar(50) # - ::regclass is a function that gives the id for a table name def column_definitions(table_name) #:nodoc: - query <<-end_sql + exec_query(<<-end_sql).rows SELECT a.attname, format_type(a.atttypid, a.atttypmod), d.adsrc, a.attnotnull FROM pg_attribute a LEFT JOIN pg_attrdef d ON a.attrelid = d.adrelid AND a.attnum = d.adnum diff --git a/activerecord/lib/active_record/connection_adapters/sqlite_adapter.rb b/activerecord/lib/active_record/connection_adapters/sqlite_adapter.rb index d76fc4103e..9ee6b88ab6 100644 --- a/activerecord/lib/active_record/connection_adapters/sqlite_adapter.rb +++ b/activerecord/lib/active_record/connection_adapters/sqlite_adapter.rb @@ -62,6 +62,10 @@ module ActiveRecord sqlite_version >= '2.0.0' end + def supports_savepoints? + sqlite_version >= '3.6.8' + end + # Returns +true+ when the connection adapter supports prepared statement # caching, otherwise returns +false+ def supports_statement_cache? @@ -144,12 +148,15 @@ module ActiveRecord # DATABASE STATEMENTS ====================================== def exec_query(sql, name = nil, binds = []) - log(sql, name) do + log(sql, name, binds) do # Don't cache statements without bind values if binds.empty? - stmt = @connection.prepare(sql) - cols = stmt.columns + stmt = @connection.prepare(sql) + cols = stmt.columns + records = stmt.to_a + stmt.close + stmt = records else cache = @statements[sql] ||= { :stmt => @connection.prepare(sql) @@ -189,6 +196,18 @@ module ActiveRecord exec_query(sql, name).rows end + def create_savepoint + execute("SAVEPOINT #{current_savepoint_name}") + end + + def rollback_to_savepoint + execute("ROLLBACK TO SAVEPOINT #{current_savepoint_name}") + end + + def release_savepoint + execute("RELEASE SAVEPOINT #{current_savepoint_name}") + end + def begin_db_transaction #:nodoc: @connection.transaction end @@ -217,6 +236,15 @@ module ActiveRecord def columns(table_name, name = nil) #:nodoc: table_structure(table_name).map do |field| + case field["dflt_value"] + when /^null$/i + field["dflt_value"] = nil + when /^'(.*)'$/ + field["dflt_value"] = $1.gsub(/''/, "'") + when /^"(.*)"$/ + field["dflt_value"] = $1.gsub(/""/, '"') + end + SQLiteColumn.new(field['name'], field['dflt_value'], field['type'], field['notnull'].to_i == 0) end end @@ -314,16 +342,11 @@ module ActiveRecord protected def select(sql, name = nil, binds = []) #:nodoc: - result = exec_query(sql, name, binds) - columns = result.columns.map { |column| - column.sub(/^"?\w+"?\./, '') - } - - result.rows.map { |row| Hash[columns.zip(row)] } + exec_query(sql, name, binds).to_a end def table_structure(table_name) - structure = @connection.table_info(quote_table_name(table_name)) + structure = exec_query("PRAGMA table_info(#{quote_table_name(table_name)})").to_hash raise(ActiveRecord::StatementInvalid, "Could not find table '#{table_name}'") if structure.empty? structure end diff --git a/activerecord/lib/active_record/fixtures.rb b/activerecord/lib/active_record/fixtures.rb index b46310f8e0..f065ef7753 100644 --- a/activerecord/lib/active_record/fixtures.rb +++ b/activerecord/lib/active_record/fixtures.rb @@ -1,4 +1,10 @@ require 'erb' + +begin + require 'psych' +rescue LoadError +end + require 'yaml' require 'csv' require 'zlib' @@ -6,15 +12,7 @@ require 'active_support/dependencies' require 'active_support/core_ext/array/wrap' require 'active_support/core_ext/object/blank' require 'active_support/core_ext/logger' - -if RUBY_VERSION < '1.9' - module YAML #:nodoc: - class Omap #:nodoc: - def keys; map { |k, v| k } end - def values; map { |k, v| v } end - end - end -end +require 'active_support/ordered_hash' if defined? ActiveRecord class FixtureClassNotFound < ActiveRecord::ActiveRecordError #:nodoc: @@ -446,20 +444,24 @@ class FixturesFileNotFound < StandardError; end # # Any fixture labeled "DEFAULTS" is safely ignored. -class Fixtures < (RUBY_VERSION < '1.9' ? YAML::Omap : Hash) +class Fixtures MAX_ID = 2 ** 30 - 1 DEFAULT_FILTER_RE = /\.ya?ml$/ - @@all_cached_fixtures = {} + @@all_cached_fixtures = Hash.new { |h,k| h[k] = {} } - def self.reset_cache(connection = nil) - connection ||= ActiveRecord::Base.connection - @@all_cached_fixtures[connection.object_id] = {} + def self.find_table_name(table_name) # :nodoc: + ActiveRecord::Base.pluralize_table_names ? + table_name.to_s.singularize.camelize : + table_name.to_s.camelize + end + + def self.reset_cache + @@all_cached_fixtures.clear end def self.cache_for_connection(connection) - @@all_cached_fixtures[connection.object_id] ||= {} - @@all_cached_fixtures[connection.object_id] + @@all_cached_fixtures[connection] end def self.fixture_is_cached?(connection, table_name) @@ -468,11 +470,10 @@ class Fixtures < (RUBY_VERSION < '1.9' ? YAML::Omap : Hash) def self.cached_fixtures(connection, keys_to_fetch = nil) if keys_to_fetch - fixtures = cache_for_connection(connection).values_at(*keys_to_fetch) + cache_for_connection(connection).values_at(*keys_to_fetch) else - fixtures = cache_for_connection(connection).values + cache_for_connection(connection).values end - fixtures.size > 1 ? fixtures : fixtures.first end def self.cache_fixtures(connection, fixtures_map) @@ -482,13 +483,11 @@ class Fixtures < (RUBY_VERSION < '1.9' ? YAML::Omap : Hash) def self.instantiate_fixtures(object, table_name, fixtures, load_instances = true) object.instance_variable_set "@#{table_name.to_s.gsub('.','_')}", fixtures if load_instances - ActiveRecord::Base.silence do - fixtures.each do |name, fixture| - begin - object.instance_variable_set "@#{name}", fixture.find - rescue FixtureClassNotFound - nil - end + fixtures.each do |name, fixture| + begin + object.instance_variable_set "@#{name}", fixture.find + rescue FixtureClassNotFound + nil end end end @@ -505,36 +504,56 @@ class Fixtures < (RUBY_VERSION < '1.9' ? YAML::Omap : Hash) def self.create_fixtures(fixtures_directory, table_names, class_names = {}) table_names = [table_names].flatten.map { |n| n.to_s } - table_names.each { |n| class_names[n.tr('/', '_').to_sym] = n.classify if n.include?('/') } - connection = block_given? ? yield : ActiveRecord::Base.connection + table_names.each { |n| + class_names[n.tr('/', '_').to_sym] = n.classify if n.include?('/') + } - table_names_to_fetch = table_names.reject { |table_name| fixture_is_cached?(connection, table_name) } + # FIXME: Apparently JK uses this. + connection = block_given? ? yield : ActiveRecord::Base.connection - unless table_names_to_fetch.empty? - ActiveRecord::Base.silence do - connection.disable_referential_integrity do - fixtures_map = {} + files_to_read = table_names.reject { |table_name| fixture_is_cached?(connection, table_name) } - fixtures = table_names_to_fetch.map do |table_name| - fixtures_map[table_name] = Fixtures.new(connection, table_name.tr('/', '_'), class_names[table_name.tr('/', '_').to_sym], File.join(fixtures_directory, table_name)) - end + unless files_to_read.empty? + connection.disable_referential_integrity do + fixtures_map = {} + + fixture_files = files_to_read.map do |path| + table_name = path.tr '/', '_' + + fixtures_map[path] = Fixtures.new( + connection, + table_name, + class_names[table_name.to_sym], + File.join(fixtures_directory, path)) + end - all_loaded_fixtures.update(fixtures_map) + all_loaded_fixtures.update(fixtures_map) - connection.transaction(:requires_new => true) do - fixtures.reverse.each { |fixture| fixture.delete_existing_fixtures } - fixtures.each { |fixture| fixture.insert_fixtures } + connection.transaction(:requires_new => true) do + fixture_files.each do |ff| + conn = ff.model_class.respond_to?(:connection) ? ff.model_class.connection : connection + table_rows = ff.table_rows - # Cap primary key sequences to max(pk). - if connection.respond_to?(:reset_pk_sequence!) - table_names.each do |table_name| - connection.reset_pk_sequence!(table_name.tr('/', '_')) + table_rows.keys.each do |table| + conn.delete "DELETE FROM #{conn.quote_table_name(table)}", 'Fixture Delete' + end + + table_rows.each do |table_name,rows| + rows.each do |row| + conn.insert_fixture(row, table_name) end end end - cache_fixtures(connection, fixtures_map) + # Cap primary key sequences to max(pk). + if connection.respond_to?(:reset_pk_sequence!) + table_names.each do |table_name| + connection.reset_pk_sequence!(table_name.tr('/', '_')) + end + end end + + cache_fixtures(connection, fixtures_map) end end cached_fixtures(connection, table_names) @@ -546,40 +565,60 @@ class Fixtures < (RUBY_VERSION < '1.9' ? YAML::Omap : Hash) Zlib.crc32(label.to_s) % MAX_ID end - attr_reader :table_name, :name + attr_reader :table_name, :name, :fixtures, :model_class def initialize(connection, table_name, class_name, fixture_path, file_filter = DEFAULT_FILTER_RE) - @connection, @table_name, @fixture_path, @file_filter = connection, table_name, fixture_path, file_filter - @name = table_name # preserve fixture base name - @class_name = class_name || - (ActiveRecord::Base.pluralize_table_names ? @table_name.singularize.camelize : @table_name.camelize) - @table_name = "#{ActiveRecord::Base.table_name_prefix}#{@table_name}#{ActiveRecord::Base.table_name_suffix}" - @table_name = class_name.table_name if class_name.respond_to?(:table_name) - @connection = class_name.connection if class_name.respond_to?(:connection) + @connection = connection + @table_name = table_name + @fixture_path = fixture_path + @file_filter = file_filter + @name = table_name # preserve fixture base name + @class_name = class_name + + @fixtures = ActiveSupport::OrderedHash.new + @table_name = "#{ActiveRecord::Base.table_name_prefix}#{@table_name}#{ActiveRecord::Base.table_name_suffix}" + + # Should be an AR::Base type class + if class_name.is_a?(Class) + @table_name = class_name.table_name + @connection = class_name.connection + @model_class = class_name + else + @model_class = class_name.constantize rescue nil + end + read_fixture_files end - def delete_existing_fixtures - @connection.delete "DELETE FROM #{@connection.quote_table_name(table_name)}", 'Fixture Delete' + def [](x) + fixtures[x] + end + + def []=(k,v) + fixtures[k] = v + end + + def each(&block) + fixtures.each(&block) + end + + def size + fixtures.size end - def insert_fixtures + # Return a hash of rows to be inserted. The key is the table, the value is + # a list of rows to insert to that table. + def table_rows now = ActiveRecord::Base.default_timezone == :utc ? Time.now.utc : Time.now now = now.to_s(:db) # allow a standard key to be used for doing defaults in YAML - if is_a?(Hash) - delete('DEFAULTS') - else - delete(assoc('DEFAULTS')) - end + fixtures.delete('DEFAULTS') # track any join tables we need to insert later - habtm_fixtures = Hash.new do |h, habtm| - h[habtm] = HabtmFixtures.new(@connection, habtm.options[:join_table], nil, nil) - end + rows = Hash.new { |h,table| h[table] = [] } - each do |label, fixture| + rows[table_name] = fixtures.map do |label, fixture| row = fixture.to_hash if model_class && model_class < ActiveRecord::Base @@ -615,14 +654,9 @@ class Fixtures < (RUBY_VERSION < '1.9' ? YAML::Omap : Hash) fk_name = (association.options[:foreign_key] || "#{association.name}_id").to_s if association.name.to_s != fk_name && value = row.delete(association.name.to_s) - if association.options[:polymorphic] - if value.sub!(/\s*\(([^\)]*)\)\s*$/, "") - target_type = $1 - target_type_name = (association.options[:foreign_type] || "#{association.name}_type").to_s - - # support polymorphic belongs_to as "label (Type)" - row[target_type_name] = target_type - end + if association.options[:polymorphic] && value.sub!(/\s*\(([^\)]*)\)\s*$/, "") + # support polymorphic belongs_to as "label (Type)" + row[association.foreign_type] = $1 end row[fk_name] = Fixtures.identify(value) @@ -630,47 +664,22 @@ class Fixtures < (RUBY_VERSION < '1.9' ? YAML::Omap : Hash) when :has_and_belongs_to_many if (targets = row.delete(association.name.to_s)) targets = targets.is_a?(Array) ? targets : targets.split(/\s*,\s*/) - join_fixtures = habtm_fixtures[association] - - targets.each do |target| - join_fixtures["#{label}_#{target}"] = Fixture.new( - { association.primary_key_name => row[primary_key_name], - association.association_foreign_key => Fixtures.identify(target) }, - nil, @connection) - end + table_name = association.options[:join_table] + rows[table_name].concat targets.map { |target| + { association.foreign_key => row[primary_key_name], + association.association_foreign_key => Fixtures.identify(target) } + } end end end end - @connection.insert_fixture(fixture, @table_name) - end - - # insert any HABTM join tables we discovered - habtm_fixtures.values.each do |fixture| - fixture.delete_existing_fixtures - fixture.insert_fixtures + row end + rows end private - class HabtmFixtures < ::Fixtures #:nodoc: - def read_fixture_files; end - end - - def model_class - unless defined?(@model_class) - @model_class = - if @class_name.nil? || @class_name.is_a?(Class) - @class_name - else - @class_name.constantize rescue nil - end - end - - @model_class - end - def primary_key_name @primary_key_name ||= model_class && model_class.primary_key end @@ -724,7 +733,7 @@ class Fixtures < (RUBY_VERSION < '1.9' ? YAML::Omap : Hash) raise Fixture::FormatError, "Bad data for #{@class_name} fixture named #{name} (nil)" end - self[name] = Fixture.new(data, model_class, @connection) + fixtures[name] = Fixture.new(data, model_class) end end end @@ -737,7 +746,7 @@ class Fixtures < (RUBY_VERSION < '1.9' ? YAML::Omap : Hash) reader.each do |row| data = {} row.each_with_index { |cell, j| data[header[j].to_s.strip] = cell.to_s.strip } - self["#{@class_name.to_s.underscore}_#{i+=1}"] = Fixture.new(data, model_class, @connection) + fixtures["#{@class_name.to_s.underscore}_#{i+=1}"] = Fixture.new(data, model_class) end end @@ -773,44 +782,30 @@ class Fixture #:nodoc: class FormatError < FixtureError #:nodoc: end - attr_reader :model_class + attr_reader :model_class, :fixture - def initialize(fixture, model_class, connection = ActiveRecord::Base.connection) - @connection = connection - @fixture = fixture - @model_class = model_class.is_a?(Class) ? model_class : model_class.constantize rescue nil + def initialize(fixture, model_class) + @fixture = fixture + @model_class = model_class end def class_name - @model_class.name if @model_class + model_class.name if model_class end def each - @fixture.each { |item| yield item } + fixture.each { |item| yield item } end def [](key) - @fixture[key] + fixture[key] end - def to_hash - @fixture - end - - def key_list - @fixture.keys.map { |column_name| @connection.quote_column_name(column_name) }.join(', ') - end - - def value_list - cols = (model_class && model_class < ActiveRecord::Base) ? model_class.columns_hash : {} - @fixture.map do |key, value| - @connection.quote(value, cols[key]).gsub('[^\]\\n', "\n").gsub('[^\]\\r', "\r") - end.join(', ') - end + alias :to_hash :fixture def find if model_class - model_class.find(self[model_class.primary_key]) + model_class.find(fixture[model_class.primary_key]) else raise FixtureClassNotFound, "No class attached to find." end @@ -837,7 +832,9 @@ module ActiveRecord self.use_instantiated_fixtures = false self.pre_loaded_fixtures = false - self.fixture_class_names = {} + self.fixture_class_names = Hash.new do |h, table_name| + h[table_name] = Fixtures.find_table_name(table_name) + end end module ClassMethods @@ -937,7 +934,7 @@ module ActiveRecord if @@already_loaded_fixtures[self.class] @loaded_fixtures = @@already_loaded_fixtures[self.class] else - load_fixtures + @loaded_fixtures = load_fixtures @@already_loaded_fixtures[self.class] = @loaded_fixtures end ActiveRecord::Base.connection.increment_open_transactions @@ -947,7 +944,7 @@ module ActiveRecord else Fixtures.reset_cache @@already_loaded_fixtures[self.class] = nil - load_fixtures + @loaded_fixtures = load_fixtures end # Instantiate fixtures for every test if requested. @@ -971,15 +968,8 @@ module ActiveRecord private def load_fixtures - @loaded_fixtures = {} fixtures = Fixtures.create_fixtures(fixture_path, fixture_table_names, fixture_class_names) - unless fixtures.nil? - if fixtures.instance_of?(Fixtures) - @loaded_fixtures[fixtures.name] = fixtures - else - fixtures.each { |f| @loaded_fixtures[f.name] = f } - end - end + Hash[fixtures.map { |f| [f.name, f] }] end # for pre_loaded_fixtures, only require the classes once. huge speed improvement diff --git a/activerecord/lib/active_record/identity_map.rb b/activerecord/lib/active_record/identity_map.rb index 438287d3ea..6718a92e13 100644 --- a/activerecord/lib/active_record/identity_map.rb +++ b/activerecord/lib/active_record/identity_map.rb @@ -81,7 +81,9 @@ module ActiveRecord @changed_attributes.update(coder['attributes'].slice(*dirty)) @changed_attributes.delete_if{|k,v| v.eql? @attributes[k]} - _run_find_callbacks + set_serialized_attributes + + run_callbacks :find self end diff --git a/activerecord/lib/active_record/locking/optimistic.rb b/activerecord/lib/active_record/locking/optimistic.rb index e5065de7fb..6b2b1ebafe 100644 --- a/activerecord/lib/active_record/locking/optimistic.rb +++ b/activerecord/lib/active_record/locking/optimistic.rb @@ -58,6 +58,12 @@ module ActiveRecord end private + def increment_lock + lock_col = self.class.locking_column + previous_lock_value = send(lock_col).to_i + send(lock_col + '=', previous_lock_value + 1) + end + def attributes_from_column_definition result = super @@ -78,8 +84,8 @@ module ActiveRecord return 0 if attribute_names.empty? lock_col = self.class.locking_column - previous_value = send(lock_col).to_i - send(lock_col + '=', previous_value + 1) + previous_lock_value = send(lock_col).to_i + increment_lock attribute_names += [lock_col] attribute_names.uniq! @@ -89,7 +95,7 @@ module ActiveRecord stmt = relation.where( relation.table[self.class.primary_key].eq(quoted_id).and( - relation.table[lock_col].eq(quote_value(previous_value)) + relation.table[lock_col].eq(quote_value(previous_lock_value)) ) ).arel.compile_update(arel_attributes_values(false, false, attribute_names)) @@ -103,7 +109,7 @@ module ActiveRecord # If something went wrong, revert the version. rescue Exception - send(lock_col + '=', previous_value) + send(lock_col + '=', previous_lock_value) raise end end diff --git a/activerecord/lib/active_record/log_subscriber.rb b/activerecord/lib/active_record/log_subscriber.rb index c7ae12977a..afadbf03ef 100644 --- a/activerecord/lib/active_record/log_subscriber.rb +++ b/activerecord/lib/active_record/log_subscriber.rb @@ -22,8 +22,16 @@ module ActiveRecord self.class.runtime += event.duration return unless logger.debug? - name = '%s (%.1fms)' % [event.payload[:name], event.duration] - sql = event.payload[:sql].squeeze(' ') + payload = event.payload + name = '%s (%.1fms)' % [payload[:name], event.duration] + sql = payload[:sql].squeeze(' ') + binds = nil + + unless (payload[:binds] || []).empty? + binds = " " + payload[:binds].map { |col,v| + [col.name, v] + }.inspect + end if odd? name = color(name, CYAN, true) @@ -32,7 +40,7 @@ module ActiveRecord name = color(name, MAGENTA, true) end - debug " #{name} #{sql}" + debug " #{name} #{sql}#{binds}" end def odd? @@ -45,4 +53,4 @@ module ActiveRecord end end -ActiveRecord::LogSubscriber.attach_to :active_record
\ No newline at end of file +ActiveRecord::LogSubscriber.attach_to :active_record diff --git a/activerecord/lib/active_record/migration/command_recorder.rb b/activerecord/lib/active_record/migration/command_recorder.rb index d7e481905a..c9d57ce812 100644 --- a/activerecord/lib/active_record/migration/command_recorder.rb +++ b/activerecord/lib/active_record/migration/command_recorder.rb @@ -40,7 +40,7 @@ module ActiveRecord @commands.reverse.map { |name, args| method = :"invert_#{name}" raise IrreversibleMigration unless respond_to?(method, true) - __send__(method, args) + send(method, args) } end @@ -48,12 +48,16 @@ module ActiveRecord super || delegate.respond_to?(*args) end - def send(method, *args) # :nodoc: - return super unless respond_to?(method) - record(method, args) + [:create_table, :rename_table, :add_column, :remove_column, :rename_index, :rename_column, :add_index, :remove_index, :add_timestamps, :remove_timestamps, :change_column, :change_column_default].each do |method| + class_eval <<-EOV, __FILE__, __LINE__ + 1 + def #{method}(*args) + record(:"#{method}", args) + end + EOV end private + def invert_create_table(args) [:drop_table, args] end @@ -86,6 +90,14 @@ module ActiveRecord def invert_add_timestamps(args) [:remove_timestamps, args] end + + # Forwards any missing method call to the \target. + def method_missing(method, *args, &block) + @delegate.send(method, *args, &block) + rescue NoMethodError => e + raise e, e.message.sub(/ for #<.*$/, " via proxy for #{@delegate}") + end + end end end diff --git a/activerecord/lib/active_record/nested_attributes.rb b/activerecord/lib/active_record/nested_attributes.rb index 0a073ad6c8..52bdadcbb4 100644 --- a/activerecord/lib/active_record/nested_attributes.rb +++ b/activerecord/lib/active_record/nested_attributes.rb @@ -410,8 +410,20 @@ module ActiveRecord assign_to_or_mark_for_destruction(existing_record, attributes, options[:allow_destroy]) end elsif existing_record = existing_records.detect { |record| record.id.to_s == attributes['id'].to_s } + unless association.loaded? || call_reject_if(association_name, attributes) + # Make sure we are operating on the actual object which is in the association's + # proxy_target array (either by finding it, or adding it if not found) + target_record = association.proxy_target.detect { |record| record == existing_record } + + if target_record + existing_record = target_record + else + association.send(:add_to_target, existing_record) + end + + end + if !call_reject_if(association_name, attributes) - association.send(:add_record_to_target_with_callbacks, existing_record) if !association.loaded? assign_to_or_mark_for_destruction(existing_record, attributes, options[:allow_destroy]) end else diff --git a/activerecord/lib/active_record/persistence.rb b/activerecord/lib/active_record/persistence.rb index 78e84c73ee..df7b22080c 100644 --- a/activerecord/lib/active_record/persistence.rb +++ b/activerecord/lib/active_record/persistence.rb @@ -233,6 +233,7 @@ module ActiveRecord def touch(name = nil) attributes = timestamp_attributes_for_update_in_model attributes << name if name + unless attributes.empty? current_time = current_time_from_proper_timezone changes = {} @@ -241,6 +242,8 @@ module ActiveRecord changes[column.to_s] = write_attribute(column.to_s, current_time) end + changes[self.class.locking_column] = increment_lock if locking_enabled? + @changed_attributes.except!(*changes.keys) primary_key = self.class.primary_key self.class.update_all(changes, { primary_key => self[primary_key] }) == 1 @@ -267,11 +270,11 @@ module ActiveRecord # Creates a record with values matching those of the instance attributes # and returns its id. def create - if self.id.nil? && connection.prefetch_primary_key?(self.class.table_name) + if id.nil? && connection.prefetch_primary_key?(self.class.table_name) self.id = connection.next_sequence_value(self.class.sequence_name) end - attributes_values = arel_attributes_values + attributes_values = arel_attributes_values(!id.nil?) new_id = if attributes_values.empty? self.class.unscoped.insert connection.empty_insert_statement_value @@ -292,7 +295,7 @@ module ActiveRecord # that instances loaded from the database would. def attributes_from_column_definition Hash[self.class.columns.map do |column| - [column.name, column.default] unless column.name == self.class.primary_key + [column.name, column.default] end] end end diff --git a/activerecord/lib/active_record/railtie.rb b/activerecord/lib/active_record/railtie.rb index a634153d6f..cace6f0cc0 100644 --- a/activerecord/lib/active_record/railtie.rb +++ b/activerecord/lib/active_record/railtie.rb @@ -77,6 +77,7 @@ module ActiveRecord ActiveSupport.on_load(:active_record) do ActionDispatch::Reloader.to_cleanup do ActiveRecord::Base.clear_reloadable_connections! + ActiveRecord::Base.clear_cache! end end end diff --git a/activerecord/lib/active_record/railties/databases.rake b/activerecord/lib/active_record/railties/databases.rake index a4fc18148e..ff36814684 100644 --- a/activerecord/lib/active_record/railties/databases.rake +++ b/activerecord/lib/active_record/railties/databases.rake @@ -193,7 +193,7 @@ db_namespace = namespace :db do file_list = [] Dir.foreach(File.join(Rails.root, 'db', 'migrate')) do |file| # only files matching "20091231235959_some_name.rb" pattern - if match_data = /(\d{14})_(.+)\.rb/.match(file) + if match_data = /^(\d{14})_(.+)\.rb$/.match(file) status = db_list.delete(match_data[1]) ? 'up' : 'down' file_list << [status, match_data[1], match_data[2]] end @@ -296,8 +296,8 @@ db_namespace = namespace :db do 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 - (ENV['FIXTURES'] ? ENV['FIXTURES'].split(/,/).map {|f| File.join(fixtures_dir, f) } : Dir["#{fixtures_dir}/**/*.{yml,csv}"]).each do |fixture_file| - Fixtures.create_fixtures(fixtures_dir, fixture_file[(fixtures_dir.size + 1)..-5]) + (ENV['FIXTURES'] ? ENV['FIXTURES'].split(/,/) : Dir["#{fixtures_dir}/**/*.{yml,csv}"].map {|f| f[(fixtures_dir.size + 1)..-5] }).each do |fixture_file| + Fixtures.create_fixtures(fixtures_dir, fixture_file) end end @@ -409,7 +409,7 @@ db_namespace = namespace :db do 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"]} #{abcs["test"]["template"]}` when "sqlite", "sqlite3" dbfile = abcs["test"]["database"] || abcs["test"]["dbfile"] `#{abcs["test"]["adapter"]} #{dbfile} < #{Rails.root}/db/#{Rails.env}_structure.sql` diff --git a/activerecord/lib/active_record/reflection.rb b/activerecord/lib/active_record/reflection.rb index b9caa64a0e..ceeb0ec39d 100644 --- a/activerecord/lib/active_record/reflection.rb +++ b/activerecord/lib/active_record/reflection.rb @@ -1,4 +1,5 @@ require 'active_support/core_ext/class/attribute' +require 'active_support/core_ext/module/deprecation' module ActiveRecord # = Active Record Reflection @@ -196,8 +197,17 @@ module ActiveRecord @quoted_table_name ||= klass.quoted_table_name end + def foreign_key + @foreign_key ||= options[:foreign_key] || derive_foreign_key + end + def primary_key_name - @primary_key_name ||= options[:foreign_key] || derive_primary_key_name + foreign_key + end + deprecate :primary_key_name => :foreign_key + + def foreign_type + @foreign_type ||= options[:foreign_type] || "#{name}_type" end def primary_key_column @@ -208,6 +218,13 @@ module ActiveRecord @association_foreign_key ||= options[:association_foreign_key] || class_name.foreign_key end + def association_primary_key + @association_primary_key ||= + options[:primary_key] || + !options[:polymorphic] && klass.primary_key || + 'id' + end + def active_record_primary_key @active_record_primary_key ||= options[:primary_key] || active_record.primary_key end @@ -244,7 +261,7 @@ module ActiveRecord false end - def through_reflection_primary_key_name + def through_reflection_foreign_key end def source_reflection @@ -291,22 +308,36 @@ module ActiveRecord !options[:validate].nil? ? options[:validate] : (options[:autosave] == true || macro == :has_many) end - def dependent_conditions(record, base_class, extra_conditions) - dependent_conditions = [] - dependent_conditions << "#{primary_key_name} = #{record.send(name).send(:owner_quoted_id)}" - dependent_conditions << "#{options[:as]}_type = '#{base_class.name}'" if options[:as] - dependent_conditions << klass.send(:sanitize_sql, options[:conditions]) if options[:conditions] - dependent_conditions << extra_conditions if extra_conditions - dependent_conditions = dependent_conditions.collect {|where| "(#{where})" }.join(" AND ") - dependent_conditions = dependent_conditions.gsub('@', '\@') - dependent_conditions - end - # Returns +true+ if +self+ is a +belongs_to+ reflection. def belongs_to? macro == :belongs_to end + def proxy_class + case macro + when :belongs_to + if options[:polymorphic] + Associations::BelongsToPolymorphicAssociation + else + Associations::BelongsToAssociation + end + when :has_and_belongs_to_many + Associations::HasAndBelongsToManyAssociation + when :has_many + if options[:through] + Associations::HasManyThroughAssociation + else + Associations::HasManyAssociation + end + when :has_one + if options[:through] + Associations::HasOneThroughAssociation + else + Associations::HasOneAssociation + end + end + end + private def derive_class_name class_name = name.to_s.camelize @@ -314,7 +345,7 @@ module ActiveRecord class_name end - def derive_primary_key_name + def derive_foreign_key if belongs_to? "#{name}_id" elsif options[:as] @@ -368,6 +399,10 @@ module ActiveRecord raise HasManyThroughAssociationNotFoundError.new(active_record.name, self) end + if through_reflection.options[:polymorphic] + raise HasManyThroughAssociationPolymorphicThroughError.new(active_record.name, self) + end + if source_reflection.nil? raise HasManyThroughSourceAssociationNotFoundError.new(self) end @@ -377,22 +412,26 @@ module ActiveRecord end if source_reflection.options[:polymorphic] && options[:source_type].nil? - raise HasManyThroughAssociationPolymorphicError.new(active_record.name, self, source_reflection) + raise HasManyThroughAssociationPolymorphicSourceError.new(active_record.name, self, source_reflection) end unless [:belongs_to, :has_many, :has_one].include?(source_reflection.macro) && source_reflection.options[:through].nil? raise HasManyThroughSourceAssociationMacroError.new(self) end + if macro == :has_one && through_reflection.collection? + raise HasOneThroughCantAssociateThroughCollection.new(active_record.name, self, through_reflection) + end + check_validity_of_inverse! end def through_reflection_primary_key - through_reflection.belongs_to? ? through_reflection.klass.primary_key : through_reflection.primary_key_name + through_reflection.belongs_to? ? through_reflection.klass.primary_key : through_reflection.foreign_key end - def through_reflection_primary_key_name - through_reflection.primary_key_name if through_reflection.belongs_to? + def through_reflection_foreign_key + through_reflection.foreign_key if through_reflection.belongs_to? end private diff --git a/activerecord/lib/active_record/relation.rb b/activerecord/lib/active_record/relation.rb index c6943444a4..cb684c1109 100644 --- a/activerecord/lib/active_record/relation.rb +++ b/activerecord/lib/active_record/relation.rb @@ -10,7 +10,9 @@ module ActiveRecord include FinderMethods, Calculations, SpawnMethods, QueryMethods, Batches + # These are explicitly delegated to improve performance (avoids method_missing) delegate :to_xml, :to_yaml, :length, :collect, :map, :each, :all?, :include?, :to => :to_a + delegate :table_name, :primary_key, :to => :klass attr_reader :table, :klass, :loaded attr_accessor :extensions @@ -30,13 +32,19 @@ module ActiveRecord def insert(values) im = arel.compile_insert values im.into @table - primary_key_name = @klass.primary_key - primary_key_value = Hash === values ? values[primary_key_name] : nil + + primary_key_value = nil + + if primary_key && Hash === values + primary_key_value = values[values.keys.find { |k| + k.name == primary_key + }] + end @klass.connection.insert( im.to_sql, 'SQL', - primary_key_name, + primary_key, primary_key_value) end @@ -172,13 +180,19 @@ module ActiveRecord if conditions || options.present? where(conditions).apply_finder_options(options.slice(:limit, :order)).update_all(updates) else + limit = nil + order = [] # Apply limit and order only if they're both present if @limit_value.present? == @order_values.present? - stmt = arel.compile_update(Arel::SqlLiteral.new(@klass.send(:sanitize_sql_for_assignment, updates))) - @klass.connection.update stmt.to_sql - else - except(:limit, :order).update_all(updates) + limit = arel.limit + order = arel.orders end + + stmt = arel.compile_update(Arel.sql(@klass.send(:sanitize_sql_for_assignment, updates))) + stmt.take limit + stmt.order(*order) + stmt.key = table[primary_key] + @klass.connection.update stmt.to_sql end end @@ -320,7 +334,7 @@ module ActiveRecord # # Delete multiple rows # Todo.delete([2,3,4]) def delete(id_or_array) - where(@klass.primary_key => id_or_array).delete_all + where(primary_key => id_or_array).delete_all end def reload @@ -336,10 +350,6 @@ module ActiveRecord self end - def primary_key - @primary_key ||= table[@klass.primary_key] - end - def to_sql @to_sql ||= arel.to_sql end @@ -373,10 +383,6 @@ module ActiveRecord to_a.inspect end - def table_name - @klass.table_name - end - protected def method_missing(method, *args, &block) diff --git a/activerecord/lib/active_record/relation/batches.rb b/activerecord/lib/active_record/relation/batches.rb index b41e935ed5..359af9820f 100644 --- a/activerecord/lib/active_record/relation/batches.rb +++ b/activerecord/lib/active_record/relation/batches.rb @@ -65,7 +65,7 @@ module ActiveRecord batch_size = options.delete(:batch_size) || 1000 relation = relation.except(:order).order(batch_order).limit(batch_size) - records = relation.where(primary_key.gteq(start)).all + records = relation.where(table[primary_key].gteq(start)).all while records.any? yield records @@ -73,7 +73,7 @@ module ActiveRecord break if records.size < batch_size if primary_key_offset = records.last.id - records = relation.where(primary_key.gt(primary_key_offset)).to_a + records = relation.where(table[primary_key].gt(primary_key_offset)).to_a else raise "Primary key not included in the custom select clause" end @@ -83,7 +83,7 @@ module ActiveRecord private def batch_order - "#{@klass.table_name}.#{@klass.primary_key} ASC" + "#{table_name}.#{primary_key} ASC" end end end diff --git a/activerecord/lib/active_record/relation/calculations.rb b/activerecord/lib/active_record/relation/calculations.rb index fd45bb24dd..abc4c54109 100644 --- a/activerecord/lib/active_record/relation/calculations.rb +++ b/activerecord/lib/active_record/relation/calculations.rb @@ -168,7 +168,7 @@ module ActiveRecord unless arel.ast.grep(Arel::Nodes::OuterJoin).empty? distinct = true - column_name = @klass.primary_key if column_name == :all + column_name = primary_key if column_name == :all end distinct = nil if column_name =~ /\s*DISTINCT\s+/i @@ -211,7 +211,7 @@ module ActiveRecord group_attr = @group_values association = @klass.reflect_on_association(group_attr.first.to_sym) associated = group_attr.size == 1 && association && association.macro == :belongs_to # only count belongs_to associations - group_fields = Array(associated ? association.primary_key_name : group_attr) + group_fields = Array(associated ? association.foreign_key : group_attr) group_aliases = group_fields.map { |field| column_alias_for(field) } group_columns = group_aliases.zip(group_fields).map { |aliaz,field| [aliaz, column_for(field)] @@ -282,15 +282,11 @@ module ActiveRecord end def type_cast_calculated_value(value, column, operation = nil) - if value.is_a?(String) || value.nil? - case operation - when 'count' then value.to_i - when 'sum' then type_cast_using_column(value || '0', column) - when 'average' then value.try(:to_d) - else type_cast_using_column(value, column) - end - else - type_cast_using_column(value, column) + case operation + when 'count' then value.to_i + when 'sum' then type_cast_using_column(value || '0', column) + when 'average' then value.respond_to?(:to_d) ? value.to_d : value + else type_cast_using_column(value, column) end end diff --git a/activerecord/lib/active_record/relation/finder_methods.rb b/activerecord/lib/active_record/relation/finder_methods.rb index 8bc28c06cb..7f32e5538e 100644 --- a/activerecord/lib/active_record/relation/finder_methods.rb +++ b/activerecord/lib/active_record/relation/finder_methods.rb @@ -19,7 +19,7 @@ module ActiveRecord # # All approaches accept an options hash as their last parameter. # - # ==== Parameters + # ==== Options # # * <tt>:conditions</tt> - An SQL fragment like "administrator = 1", <tt>["user_name = ?", username]</tt>, # or <tt>["user_name = :user_name", { :user_name => user_name }]</tt>. See conditions in the intro. @@ -171,13 +171,13 @@ module ActiveRecord def exists?(id = nil) id = id.id if ActiveRecord::Base === id - relation = select(primary_key).limit(1) + relation = select("1").limit(1) case id when Array, Hash relation = relation.where(id) else - relation = relation.where(primary_key.eq(id)) if id + relation = relation.where(table[primary_key].eq(id)) if id end relation.first ? true : false @@ -188,7 +188,8 @@ module ActiveRecord def find_with_associations including = (@eager_load_values + @includes_values).uniq join_dependency = ActiveRecord::Associations::ClassMethods::JoinDependency.new(@klass, including, []) - rows = construct_relation_for_association_find(join_dependency).to_a + relation = construct_relation_for_association_find(join_dependency) + rows = connection.select_all(relation.to_sql, 'SQL', relation.bind_values) join_dependency.instantiate(rows) rescue ThrowResult [] @@ -225,10 +226,10 @@ module ActiveRecord def construct_limited_ids_condition(relation) orders = relation.order_values - values = @klass.connection.distinct("#{@klass.connection.quote_table_name @klass.table_name}.#{@klass.primary_key}", orders) + values = @klass.connection.distinct("#{@klass.connection.quote_table_name table_name}.#{primary_key}", orders) - ids_array = relation.select(values).collect {|row| row[@klass.primary_key]} - ids_array.empty? ? raise(ThrowResult) : primary_key.in(ids_array) + ids_array = relation.select(values).collect {|row| row[primary_key]} + ids_array.empty? ? raise(ThrowResult) : table[primary_key].in(ids_array) end def find_by_attributes(match, attributes, *args) @@ -290,24 +291,24 @@ module ActiveRecord def find_one(id) id = id.id if ActiveRecord::Base === id - column = columns_hash[primary_key.name.to_s] + column = columns_hash[primary_key] substitute = connection.substitute_for(column, @bind_values) - relation = where(primary_key.eq(substitute)) + relation = where(table[primary_key].eq(substitute)) relation.bind_values = [[column, id]] record = relation.first unless record conditions = arel.where_sql conditions = " [#{conditions}]" if conditions - raise RecordNotFound, "Couldn't find #{@klass.name} with #{@klass.primary_key}=#{id}#{conditions}" + raise RecordNotFound, "Couldn't find #{@klass.name} with #{primary_key}=#{id}#{conditions}" end record end def find_some(ids) - result = where(primary_key.in(ids)).all + result = where(table[primary_key].in(ids)).all expected_size = if @limit_value && ids.size > @limit_value diff --git a/activerecord/lib/active_record/relation/predicate_builder.rb b/activerecord/lib/active_record/relation/predicate_builder.rb index 70d84619a1..61d9974570 100644 --- a/activerecord/lib/active_record/relation/predicate_builder.rb +++ b/activerecord/lib/active_record/relation/predicate_builder.rb @@ -15,18 +15,18 @@ module ActiveRecord table = Arel::Table.new(table_name, :engine => engine) end - attribute = table[column] || Arel::Attribute.new(table, column) + attribute = table[column.to_sym] case value when Array, ActiveRecord::Associations::AssociationCollection, ActiveRecord::Relation values = value.to_a.map { |x| - x.respond_to?(:quoted_id) ? x.quoted_id : x + x.is_a?(ActiveRecord::Base) ? x.id : x } attribute.in(values) when Range, Arel::Relation attribute.in(value) when ActiveRecord::Base - attribute.eq(Arel.sql(value.quoted_id)) + attribute.eq(value.id) when Class # FIXME: I think we need to deprecate this behavior attribute.eq(value.name) diff --git a/activerecord/lib/active_record/relation/query_methods.rb b/activerecord/lib/active_record/relation/query_methods.rb index 0ab55ae864..6a905b8588 100644 --- a/activerecord/lib/active_record/relation/query_methods.rb +++ b/activerecord/lib/active_record/relation/query_methods.rb @@ -128,7 +128,7 @@ module ActiveRecord def create_with(value) relation = clone - relation.create_with_value = value + relation.create_with_value = value && (@create_with_value || {}).merge(value) relation end @@ -152,7 +152,7 @@ module ActiveRecord order_clause = arel.order_clauses order = order_clause.empty? ? - "#{@klass.table_name}.#{@klass.primary_key} DESC" : + "#{table_name}.#{primary_key} DESC" : reverse_sql_order(order_clause).join(', ') except(:order).order(Arel.sql(order)) @@ -171,7 +171,7 @@ module ActiveRecord arel.having(*@having_values.uniq.reject{|h| h.blank?}) unless @having_values.empty? - arel.take(@limit_value) if @limit_value + arel.take(connection.sanitize_limit(@limit_value)) if @limit_value arel.skip(@offset_value) if @offset_value arel.group(*@group_values.uniq.reject{|g| g.blank?}) unless @group_values.empty? diff --git a/activerecord/lib/active_record/relation/spawn_methods.rb b/activerecord/lib/active_record/relation/spawn_methods.rb index 5acf3ec83a..4150e36a9a 100644 --- a/activerecord/lib/active_record/relation/spawn_methods.rb +++ b/activerecord/lib/active_record/relation/spawn_methods.rb @@ -46,21 +46,28 @@ module ActiveRecord merged_relation.where_values = merged_wheres - (Relation::SINGLE_VALUE_METHODS - [:lock]).each do |method| + (Relation::SINGLE_VALUE_METHODS - [:lock, :create_with]).each do |method| value = r.send(:"#{method}_value") merged_relation.send(:"#{method}_value=", value) unless value.nil? end merged_relation.lock_value = r.lock_value unless merged_relation.lock_value + merged_relation = merged_relation.create_with(r.create_with_value) if r.create_with_value + # Apply scope extension modules merged_relation.send :apply_modules, r.extensions merged_relation end - alias :& :merge - + # Removes from the query the condition(s) specified in +skips+. + # + # Example: + # + # Post.order('id asc').except(:order) # discards the order condition + # Post.where('id > 10').order('id asc').except(:where) # discards the where condition but keeps the order + # def except(*skips) result = self.class.new(@klass, table) @@ -75,6 +82,13 @@ module ActiveRecord result end + # Removes any condition from the query other than the one(s) specified in +onlies+. + # + # Example: + # + # Post.order('id asc').only(:where) # discards the order condition + # Post.order('id asc').only(:where, :order) # uses the specified order + # def only(*onlies) result = self.class.new(@klass, table) @@ -98,7 +112,7 @@ module ActiveRecord options.assert_valid_keys(VALID_FIND_OPTIONS) finders = options.dup - finders.delete_if { |key, value| value.nil? } + finders.delete_if { |key, value| value.nil? && key != :limit } ([:joins, :select, :group, :order, :having, :limit, :offset, :from, :lock, :readonly] & finders.keys).each do |finder| relation = relation.send(finder, finders[finder]) diff --git a/activerecord/lib/active_record/result.rb b/activerecord/lib/active_record/result.rb index 8deff1478f..0465b21e88 100644 --- a/activerecord/lib/active_record/result.rb +++ b/activerecord/lib/active_record/result.rb @@ -20,10 +20,14 @@ module ActiveRecord hash_rows.each { |row| yield row } end + def to_hash + hash_rows + end + private def hash_rows @hash_rows ||= @rows.map { |row| - ActiveSupport::OrderedHash[@columns.zip(row)] + Hash[@columns.zip(row)] } end end diff --git a/activerecord/lib/active_record/schema_dumper.rb b/activerecord/lib/active_record/schema_dumper.rb index e30b481fe1..a893c0ad85 100644 --- a/activerecord/lib/active_record/schema_dumper.rb +++ b/activerecord/lib/active_record/schema_dumper.rb @@ -83,7 +83,7 @@ HEADER # first dump primary key column if @connection.respond_to?(:pk_and_sequence_for) - pk, pk_seq = @connection.pk_and_sequence_for(table) + pk, _ = @connection.pk_and_sequence_for(table) elsif @connection.respond_to?(:primary_key) pk = @connection.primary_key(table) end diff --git a/activerecord/lib/active_record/session_store.rb b/activerecord/lib/active_record/session_store.rb index 3400fd6ade..7e77aefb21 100644 --- a/activerecord/lib/active_record/session_store.rb +++ b/activerecord/lib/active_record/session_store.rb @@ -59,10 +59,12 @@ module ActiveRecord end def drop_table! + connection_pool.clear_table_cache!(table_name) connection.drop_table table_name end def create_table! + connection_pool.clear_table_cache!(table_name) connection.create_table(table_name) do |t| t.string session_id_column, :limit => 255 t.text data_column_name diff --git a/activerecord/lib/active_record/timestamp.rb b/activerecord/lib/active_record/timestamp.rb index 2ecbd906bd..1511c71ffc 100644 --- a/activerecord/lib/active_record/timestamp.rb +++ b/activerecord/lib/active_record/timestamp.rb @@ -9,24 +9,26 @@ module ActiveRecord # # Timestamping can be turned off by setting: # - # <tt>ActiveRecord::Base.record_timestamps = false</tt> + # config.active_record.record_timestamps = false # # Timestamps are in the local timezone by default but you can use UTC by setting: # - # <tt>ActiveRecord::Base.default_timezone = :utc</tt> + # config.active_record.default_timezone = :utc # # == Time Zone aware attributes # # By default, ActiveRecord::Base keeps all the datetime columns time zone aware by executing following code. # - # ActiveRecord::Base.time_zone_aware_attributes = true + # config.active_record.time_zone_aware_attributes = true # # This feature can easily be turned off by assigning value <tt>false</tt> . # - # If your attributes are time zone aware and you desire to skip time zone conversion for certain - # attributes then you can do following: + # If your attributes are time zone aware and you desire to skip time zone conversion to the current Time.zone + # when reading certain attributes then you can do following: # - # Topic.skip_time_zone_conversion_for_attributes = [:written_on] + # class Topic < ActiveRecord::Base + # self.skip_time_zone_conversion_for_attributes = [:written_on] + # end module Timestamp extend ActiveSupport::Concern @@ -66,8 +68,16 @@ module ActiveRecord self.record_timestamps && (!partial_updates? || changed? || (attributes.keys & self.class.serialized_attributes.keys).present?) end + def timestamp_attributes_for_create_in_model + timestamp_attributes_for_create.select { |c| self.class.column_names.include?(c.to_s) } + end + def timestamp_attributes_for_update_in_model - timestamp_attributes_for_update.select { |c| respond_to?(c) } + timestamp_attributes_for_update.select { |c| self.class.column_names.include?(c.to_s) } + end + + def all_timestamp_attributes_in_model + timestamp_attributes_for_create_in_model + timestamp_attributes_for_update_in_model end def timestamp_attributes_for_update #:nodoc: diff --git a/activerecord/lib/active_record/transactions.rb b/activerecord/lib/active_record/transactions.rb index de496698f1..60d4c256c4 100644 --- a/activerecord/lib/active_record/transactions.rb +++ b/activerecord/lib/active_record/transactions.rb @@ -260,7 +260,7 @@ module ActiveRecord # Call the after_commit callbacks def committed! #:nodoc: - _run_commit_callbacks + run_callbacks :commit ensure clear_transaction_record_state end @@ -268,7 +268,7 @@ module ActiveRecord # Call the after rollback callbacks. The restore_state argument indicates if the record # state should be rolled back to the beginning or just to the last savepoint. def rolledback!(force_restore_state = false) #:nodoc: - _run_rollback_callbacks + run_callbacks :rollback ensure restore_transaction_record_state(force_restore_state) end @@ -302,8 +302,8 @@ module ActiveRecord # Save the new record state and id of a record so it can be restored later if a transaction fails. def remember_transaction_record_state #:nodoc @_start_transaction_state ||= {} + @_start_transaction_state[:id] = id if has_attribute?(self.class.primary_key) unless @_start_transaction_state.include?(:new_record) - @_start_transaction_state[:id] = id if has_attribute?(self.class.primary_key) @_start_transaction_state[:new_record] = @new_record end unless @_start_transaction_state.include?(:destroyed) @@ -326,16 +326,14 @@ module ActiveRecord @_start_transaction_state[:level] = (@_start_transaction_state[:level] || 0) - 1 if @_start_transaction_state[:level] < 1 restore_state = remove_instance_variable(:@_start_transaction_state) - if restore_state - @attributes = @attributes.dup if @attributes.frozen? - @new_record = restore_state[:new_record] - @destroyed = restore_state[:destroyed] - if restore_state[:id] - self.id = restore_state[:id] - else - @attributes.delete(self.class.primary_key) - @attributes_cache.delete(self.class.primary_key) - end + @attributes = @attributes.dup if @attributes.frozen? + @new_record = restore_state[:new_record] + @destroyed = restore_state[:destroyed] + if restore_state.has_key?(:id) + self.id = restore_state[:id] + else + @attributes.delete(self.class.primary_key) + @attributes_cache.delete(self.class.primary_key) end end end diff --git a/activerecord/lib/active_record/validations.rb b/activerecord/lib/active_record/validations.rb index f367315b22..26c1a9db93 100644 --- a/activerecord/lib/active_record/validations.rb +++ b/activerecord/lib/active_record/validations.rb @@ -37,7 +37,7 @@ module ActiveRecord end end - # The validation process on save can be skipped by passing false. The regular Base#save method is + # The validation process on save can be skipped by passing :validate => false. The regular Base#save method is # replaced with this when the validations module is mixed in, which it is by default. def save(options={}) perform_validations(options) ? super : false diff --git a/activerecord/lib/active_record/validations/uniqueness.rb b/activerecord/lib/active_record/validations/uniqueness.rb index 853808eebf..a96796f9ff 100644 --- a/activerecord/lib/active_record/validations/uniqueness.rb +++ b/activerecord/lib/active_record/validations/uniqueness.rb @@ -15,8 +15,10 @@ module ActiveRecord def validate_each(record, attribute, value) finder_class = find_finder_class_for(record) - if value && record.class.serialized_attributes.key?(attribute.to_s) - value = YAML.dump value + coder = record.class.serialized_attributes[attribute.to_s] + + if value && coder + value = coder.dump value end sql, params = mount_sql_and_params(finder_class, record.class.quoted_table_name, attribute, value) @@ -85,11 +87,16 @@ module ActiveRecord # can be named "davidhh". # # class Person < ActiveRecord::Base - # validates_uniqueness_of :user_name, :scope => :account_id + # validates_uniqueness_of :user_name # end # - # It can also validate whether the value of the specified attributes are unique based on multiple - # scope parameters. For example, making sure that a teacher can only be on the schedule once + # It can also validate whether the value of the specified attributes are unique based on a scope parameter: + # + # class Person < ActiveRecord::Base + # validates_uniqueness_of :user_name, :scope => :account_id + # end + # + # Or even multiple scope parameters. For example, making sure that a teacher can only be on the schedule once # per semester for a particular class. # # class TeacherSchedule < ActiveRecord::Base |