aboutsummaryrefslogtreecommitdiffstats
path: root/activerecord/lib
diff options
context:
space:
mode:
authorCarl Lerche & Yehuda Katz <wycats@gmail.com>2009-04-13 15:18:45 -0700
committerCarl Lerche & Yehuda Katz <wycats@gmail.com>2009-04-13 15:18:45 -0700
commit906aebceedb95d8caa6db6314bc90f605bdfaf2b (patch)
tree5abc86bb6709b20df7cb5f4d1750b27c641dca4b /activerecord/lib
parent2036d3ba75da1a0f3061bf5a33c89e2b2eaff420 (diff)
parentc877857d59554d78dbf45f5f9fcaafb8badec4e2 (diff)
downloadrails-906aebceedb95d8caa6db6314bc90f605bdfaf2b.tar.gz
rails-906aebceedb95d8caa6db6314bc90f605bdfaf2b.tar.bz2
rails-906aebceedb95d8caa6db6314bc90f605bdfaf2b.zip
Bring abstract_controller up to date with rails/master
Resolved all the conflicts since 2.3.0 -> HEAD. Following is a list of commits that could not be applied cleanly or are obviated with the abstract_controller refactor. They all need to be revisited to ensure that fixes made in 2.3 do not reappear in 3.0: 2259ecf368e6a6715966f69216e3ee86bf1a82a7 AR not available * This will be reimplemented with ActionORM or equivalent 06182ea02e92afad579998aa80144588e8865ac3 implicitly rendering a js response should not use the default layout [#1844 state:resolved] * This will be handled generically 893e9eb99504705419ad6edac14d00e71cef5f12 Improve view rendering performance in development mode and reinstate template recompiling in production [#1909 state:resolved] * We will need to reimplement rails-dev-boost on top of the refactor; the changes here are very implementation specific and cannot be cleanly applied. The following commits are implicated: 199e750d46c04970b5e7684998d09405648ecbd4 3942cb406e1d5db0ac00e03153809cc8dc4cc4db f8ea9f85d4f1e3e6f3b5d895bef6b013aa4b0690 e3b166aab37ddc2fbab030b146eb61713b91bf55 ae9f258e03c9fd5088da12c1c6cd216cc89a01f7 44423126c6f6133a1d9cf1d0832b527e8711d40f 0cb020b4d6d838025859bd60fb8151c8e21b8e84 workaround for picking layouts based on wrong view_paths [#1974 state:resolved] * The specifics of this commit no longer apply. Since it is a two-line commit, we will reimplement this change. 8c5cc66a831aadb159f3daaffa4208064c30af0e make action_controller/layouts pick templates from the current instance's view_paths instead of the class view_paths [#1974 state:resolved] * This does not apply at all. It should be trivial to apply the feature to the reimplemented ActionController::Base. 87e8b162463f13bd50d27398f020769460a770e3 fix HTML fallback for explicit templates [#2052 state:resolved] * There were a number of patches related to this that simply compounded each other. Basically none of them apply cleanly, and the underlying issue needs to be revisited. After discussing the underlying problem with Koz, we will defer these fixes for further discussion.
Diffstat (limited to 'activerecord/lib')
-rw-r--r--activerecord/lib/active_record.rb3
-rwxr-xr-xactiverecord/lib/active_record/associations.rb312
-rw-r--r--activerecord/lib/active_record/associations/association_collection.rb57
-rw-r--r--activerecord/lib/active_record/associations/has_and_belongs_to_many_association.rb4
-rw-r--r--activerecord/lib/active_record/associations/has_many_association.rb4
-rw-r--r--activerecord/lib/active_record/associations/has_many_through_association.rb10
-rw-r--r--activerecord/lib/active_record/associations/has_one_association.rb13
-rw-r--r--activerecord/lib/active_record/attribute_methods.rb1
-rw-r--r--activerecord/lib/active_record/autosave_association.rb349
-rwxr-xr-xactiverecord/lib/active_record/base.rb98
-rw-r--r--activerecord/lib/active_record/batches.rb81
-rw-r--r--activerecord/lib/active_record/calculations.rb26
-rw-r--r--activerecord/lib/active_record/callbacks.rb38
-rw-r--r--activerecord/lib/active_record/connection_adapters/abstract/connection_pool.rb16
-rw-r--r--activerecord/lib/active_record/connection_adapters/abstract/schema_definitions.rb1
-rw-r--r--activerecord/lib/active_record/connection_adapters/postgresql_adapter.rb31
-rw-r--r--activerecord/lib/active_record/connection_adapters/sqlite3_adapter.rb2
-rw-r--r--activerecord/lib/active_record/connection_adapters/sqlite_adapter.rb64
-rw-r--r--activerecord/lib/active_record/fixtures.rb103
-rw-r--r--activerecord/lib/active_record/locking/optimistic.rb33
-rw-r--r--activerecord/lib/active_record/migration.rb6
-rw-r--r--activerecord/lib/active_record/named_scope.rb30
-rw-r--r--activerecord/lib/active_record/nested_attributes.rb329
-rw-r--r--activerecord/lib/active_record/reflection.rb7
-rw-r--r--activerecord/lib/active_record/serializers/json_serializer.rb19
-rw-r--r--activerecord/lib/active_record/serializers/xml_serializer.rb18
-rw-r--r--activerecord/lib/active_record/session_store.rb14
-rw-r--r--activerecord/lib/active_record/test_case.rb14
-rw-r--r--activerecord/lib/active_record/transactions.rb2
-rw-r--r--activerecord/lib/active_record/validations.rb28
-rw-r--r--activerecord/lib/active_record/version.rb2
31 files changed, 1313 insertions, 402 deletions
diff --git a/activerecord/lib/active_record.rb b/activerecord/lib/active_record.rb
index e1265b7e1e..2f8c5c712f 100644
--- a/activerecord/lib/active_record.rb
+++ b/activerecord/lib/active_record.rb
@@ -46,7 +46,9 @@ module ActiveRecord
autoload :AssociationPreload, 'active_record/association_preload'
autoload :Associations, 'active_record/associations'
autoload :AttributeMethods, 'active_record/attribute_methods'
+ autoload :AutosaveAssociation, 'active_record/autosave_association'
autoload :Base, 'active_record/base'
+ autoload :Batches, 'active_record/batches'
autoload :Calculations, 'active_record/calculations'
autoload :Callbacks, 'active_record/callbacks'
autoload :Dirty, 'active_record/dirty'
@@ -55,6 +57,7 @@ module ActiveRecord
autoload :Migration, 'active_record/migration'
autoload :Migrator, 'active_record/migration'
autoload :NamedScope, 'active_record/named_scope'
+ autoload :NestedAttributes, 'active_record/nested_attributes'
autoload :Observing, 'active_record/observer'
autoload :QueryCache, 'active_record/query_cache'
autoload :Reflection, 'active_record/reflection'
diff --git a/activerecord/lib/active_record/associations.rb b/activerecord/lib/active_record/associations.rb
index 8b51a38f48..6d25b36aea 100755
--- a/activerecord/lib/active_record/associations.rb
+++ b/activerecord/lib/active_record/associations.rb
@@ -22,7 +22,7 @@ module ActiveRecord
through_reflection = reflection.through_reflection
source_reflection_names = reflection.source_reflection_names
source_associations = reflection.through_reflection.klass.reflect_on_all_associations.collect { |a| a.name.inspect }
- super("Could not find the source association(s) #{source_reflection_names.collect(&:inspect).to_sentence :two_words_connector => ' or ', :last_word_connector => ', or '} in model #{through_reflection.klass}. Try 'has_many #{reflection.name.inspect}, :through => #{through_reflection.name.inspect}, :source => <name>'. Is it one of #{source_associations.to_sentence :two_words_connector => ' or ', :last_word_connector => ', or '}?")
+ super("Could not find the source association(s) #{source_reflection_names.collect(&:inspect).to_sentence(:two_words_connector => ' or ', :last_word_connector => ', or ', :locale => :en)} in model #{through_reflection.klass}. Try 'has_many #{reflection.name.inspect}, :through => #{through_reflection.name.inspect}, :source => <name>'. Is it one of #{source_associations.to_sentence(:two_words_connector => ' or ', :last_word_connector => ', or ', :locale => :en)}?")
end
end
@@ -51,6 +51,12 @@ module ActiveRecord
end
end
+ class HasAndBelongsToManyAssociationForeignKeyNeeded < ActiveRecordError #:nodoc:
+ def initialize(reflection)
+ super("Cannot create self referential has_and_belongs_to_many association on '#{reflection.class_name rescue nil}##{reflection.name rescue nil}'. :association_foreign_key cannot be the same as the :foreign_key.")
+ end
+ end
+
class EagerLoadPolymorphicError < ActiveRecordError #:nodoc:
def initialize(reflection)
super("Can not eagerly load the polymorphic association #{reflection.name.inspect}")
@@ -65,7 +71,7 @@ module ActiveRecord
# See ActiveRecord::Associations::ClassMethods for documentation.
module Associations # :nodoc:
- # These classes will be loaded when associatoins are created.
+ # 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 :AssociationProxy, 'active_record/associations/association_proxy'
@@ -88,6 +94,18 @@ module ActiveRecord
end unless self.new_record?
end
+ private
+ # Gets the specified association instance if it responds to :loaded?, nil otherwise.
+ def association_instance_get(name)
+ association = instance_variable_get("@#{name}")
+ association if association.respond_to?(:loaded?)
+ end
+
+ # Set the specified association instance.
+ def association_instance_set(name, association)
+ instance_variable_set("@#{name}", association)
+ end
+
# Associations are a set of macro-like class methods for tying objects together through foreign keys. They express relationships like
# "Project has one Project Manager" or "Project belongs to a Portfolio". Each macro adds a number of methods to the class which are
# specialized according to the collection or association symbol and the options hash. It works much the same way as Ruby's own <tt>attr*</tt>
@@ -121,41 +139,40 @@ module ActiveRecord
# | | belongs_to |
# generated methods | belongs_to | :polymorphic | has_one
# ----------------------------------+------------+--------------+---------
- # #other | X | X | X
- # #other=(other) | X | X | X
- # #build_other(attributes={}) | X | | X
- # #create_other(attributes={}) | X | | X
- # #other.create!(attributes={}) | | | X
- # #other.nil? | X | X |
+ # other | X | X | X
+ # other=(other) | X | X | X
+ # build_other(attributes={}) | X | | X
+ # create_other(attributes={}) | X | | X
+ # other.create!(attributes={}) | | | X
#
# ===Collection associations (one-to-many / many-to-many)
# | | | has_many
# generated methods | habtm | has_many | :through
# ----------------------------------+-------+----------+----------
- # #others | X | X | X
- # #others=(other,other,...) | X | X | X
- # #other_ids | X | X | X
- # #other_ids=(id,id,...) | X | X | X
- # #others<< | X | X | X
- # #others.push | X | X | X
- # #others.concat | X | X | X
- # #others.build(attributes={}) | X | X | X
- # #others.create(attributes={}) | X | X | X
- # #others.create!(attributes={}) | X | X | X
- # #others.size | X | X | X
- # #others.length | X | X | X
- # #others.count | X | X | X
- # #others.sum(args*,&block) | X | X | X
- # #others.empty? | X | X | X
- # #others.clear | X | X | X
- # #others.delete(other,other,...) | X | X | X
- # #others.delete_all | 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
+ # others | X | X | X
+ # others=(other,other,...) | X | X | X
+ # other_ids | X | X | X
+ # other_ids=(id,id,...) | X | X | X
+ # others<< | X | X | X
+ # others.push | X | X | X
+ # others.concat | X | X | X
+ # others.build(attributes={}) | X | X | X
+ # others.create(attributes={}) | X | X | X
+ # others.create!(attributes={}) | X | X | X
+ # others.size | X | X | X
+ # others.length | X | X | X
+ # others.count | X | X | X
+ # others.sum(args*,&block) | X | X | X
+ # others.empty? | X | X | X
+ # others.clear | X | X | X
+ # others.delete(other,other,...) | X | X | X
+ # others.delete_all | 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
#
# == Cardinality and associations
#
@@ -256,6 +273,10 @@ module ActiveRecord
# You can manipulate objects and associations before they are saved to the database, but there is some special behavior you should be
# aware of, mostly involving the saving of associated objects.
#
+ # Unless you enable the :autosave option on a <tt>has_one</tt>, <tt>belongs_to</tt>,
+ # <tt>has_many</tt>, or <tt>has_and_belongs_to_many</tt> association,
+ # in which case the members are always saved.
+ #
# === 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
@@ -752,6 +773,9 @@ module ActiveRecord
# If true, all the associated objects are readonly through the association.
# [:validate]
# If false, don't validate the associated objects when saving the parent object. true by default.
+ # [:autosave]
+ # If true, always save any loaded members and destroy members marked for destruction, when saving the parent object. Off by default.
+ #
# Option examples:
# has_many :comments, :order => "posted_on"
# has_many :comments, :include => :author
@@ -768,11 +792,7 @@ module ActiveRecord
# 'ORDER BY p.first_name'
def has_many(association_id, options = {}, &extension)
reflection = create_has_many_reflection(association_id, options, &extension)
-
configure_dependency_for_has_many(reflection)
-
- add_multiple_associated_validation_callbacks(reflection.name) unless options[:validate] == false
- add_multiple_associated_save_callbacks(reflection.name)
add_association_callbacks(reflection.name, reflection.options)
if options[:through]
@@ -794,8 +814,6 @@ module ActiveRecord
# [association=(associate)]
# Assigns the associate object, extracts the primary key, sets it as the foreign key,
# and saves the associate object.
- # [association.nil?]
- # Returns +true+ if there is no associated object.
# [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
@@ -814,7 +832,6 @@ module ActiveRecord
# An Account class declares <tt>has_one :beneficiary</tt>, which will add:
# * <tt>Account#beneficiary</tt> (similar to <tt>Beneficiary.find(:first, :conditions => "account_id = #{id}")</tt>)
# * <tt>Account#beneficiary=(beneficiary)</tt> (similar to <tt>beneficiary.account_id = account.id; beneficiary.save</tt>)
- # * <tt>Account#beneficiary.nil?</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>)
#
@@ -857,14 +874,16 @@ module ActiveRecord
# [:source]
# Specifies the source association name used by <tt>has_one :through</tt> queries. Only use it if the name cannot be
# inferred from the association. <tt>has_one :favorite, :through => :favorites</tt> will look for a
- # <tt>:favorite</tt> on Favorite, unless a <tt>:source</tt> is given.
+ # <tt>:favorite</tt> on Favorite, unless a <tt>:source</tt> is given.
# [:source_type]
# Specifies type of the source association used by <tt>has_one :through</tt> queries where the source
- # association is a polymorphic +belongs_to+.
+ # association is a polymorphic +belongs_to+.
# [:readonly]
# If true, the associated object is readonly through the association.
# [:validate]
# If false, don't validate the associated object when saving the parent object. +false+ by default.
+ # [:autosave]
+ # If true, always save the associated object or destroy it if marked for destruction, when saving the parent object. Off by default.
#
# Option examples:
# has_one :credit_card, :dependent => :destroy # destroys the associated credit card
@@ -881,25 +900,9 @@ module ActiveRecord
association_accessor_methods(reflection, ActiveRecord::Associations::HasOneThroughAssociation)
else
reflection = create_has_one_reflection(association_id, options)
-
- ivar = "@#{reflection.name}"
-
- method_name = "has_one_after_save_for_#{reflection.name}".to_sym
- define_method(method_name) do
- association = instance_variable_get(ivar) if instance_variable_defined?(ivar)
-
- if !association.nil? && (new_record? || association.new_record? || association[reflection.primary_key_name] != id)
- association[reflection.primary_key_name] = id
- association.save(true)
- end
- end
- after_save method_name
-
- add_single_associated_validation_callbacks(reflection.name) if options[:validate] == true
association_accessor_methods(reflection, HasOneAssociation)
association_constructor_method(:build, reflection, HasOneAssociation)
association_constructor_method(:create, reflection, HasOneAssociation)
-
configure_dependency_for_has_one(reflection)
end
end
@@ -916,8 +919,6 @@ module ActiveRecord
# Returns the associated object. +nil+ is returned if none is found.
# [association=(associate)]
# Assigns the associate object, extracts the primary key, and sets it as the foreign key.
- # [association.nil?]
- # Returns +true+ if there is no associated object.
# [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.
@@ -935,7 +936,6 @@ module ActiveRecord
# * <tt>Post#author</tt> (similar to <tt>Author.find(author_id)</tt>)
# * <tt>Post#author=(author)</tt> (similar to <tt>post.author_id = author.id</tt>)
# * <tt>Post#author?</tt> (similar to <tt>post.author == some_author</tt>)
- # * <tt>Post#author.nil?</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>)
# The declaration can also include an options hash to specialize the behavior of the association.
@@ -979,6 +979,8 @@ module ActiveRecord
# If true, the associated object is readonly through the association.
# [:validate]
# If false, don't validate the associated objects when saving the parent object. +false+ by default.
+ # [:autosave]
+ # If true, always save the associated object or destroy it if marked for destruction, when saving the parent object. Off by default.
#
# Option examples:
# belongs_to :firm, :foreign_key => "client_of"
@@ -991,54 +993,17 @@ module ActiveRecord
def belongs_to(association_id, options = {})
reflection = create_belongs_to_reflection(association_id, options)
- ivar = "@#{reflection.name}"
-
if reflection.options[:polymorphic]
association_accessor_methods(reflection, BelongsToPolymorphicAssociation)
-
- method_name = "polymorphic_belongs_to_before_save_for_#{reflection.name}".to_sym
- define_method(method_name) do
- association = instance_variable_get(ivar) if instance_variable_defined?(ivar)
-
- if association && association.target
- if association.new_record?
- association.save(true)
- end
-
- if association.updated?
- self[reflection.primary_key_name] = association.id
- self[reflection.options[:foreign_type]] = association.class.base_class.name.to_s
- end
- end
- end
- before_save method_name
else
association_accessor_methods(reflection, BelongsToAssociation)
association_constructor_method(:build, reflection, BelongsToAssociation)
association_constructor_method(:create, reflection, BelongsToAssociation)
-
- method_name = "belongs_to_before_save_for_#{reflection.name}".to_sym
- define_method(method_name) do
- association = instance_variable_get(ivar) if instance_variable_defined?(ivar)
-
- if !association.nil?
- if association.new_record?
- association.save(true)
- end
-
- if association.updated?
- self[reflection.primary_key_name] = association.id
- end
- end
- end
- before_save method_name
end
# Create the callbacks to update counter cache
if options[:counter_cache]
- cache_column = options[:counter_cache] == true ?
- "#{self.to_s.demodulize.underscore.pluralize}_count" :
- options[:counter_cache]
+ cache_column = reflection.counter_cache_column
method_name = "belongs_to_counter_cache_after_create_for_#{reflection.name}".to_sym
define_method(method_name) do
@@ -1059,8 +1024,6 @@ module ActiveRecord
)
end
- add_single_associated_validation_callbacks(reflection.name) if options[:validate] == true
-
configure_dependency_for_belongs_to(reflection)
end
@@ -1075,6 +1038,22 @@ module ActiveRecord
# but it in fact generates a join table name of "paper_boxes_papers". Be aware of this caveat, and use the
# custom <tt>:join_table</tt> option if you need to.
#
+ # The join table should not have a primary key or a model associated with it. You must manually generate the
+ # join table with a migration such as this:
+ #
+ # class CreateDevelopersProjectsJoinTable < ActiveRecord::Migration
+ # def self.up
+ # create_table :developers_projects, :id => false do |t|
+ # t.integer :developer_id
+ # t.integer :project_id
+ # end
+ # end
+ #
+ # def self.down
+ # drop_table :developers_projects
+ # 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
@@ -1151,11 +1130,12 @@ module ActiveRecord
# [:foreign_key]
# Specify the foreign key used for the association. By default this is guessed to be the name
# of this class in lower-case and "_id" suffixed. So a Person class that makes a +has_and_belongs_to_many+ association
- # will use "person_id" as the default <tt>:foreign_key</tt>.
+ # to Project will use "person_id" as the default <tt>:foreign_key</tt>.
# [:association_foreign_key]
- # Specify the association foreign key used for the association. By default this is
- # guessed to be the name of the associated class in lower-case and "_id" suffixed. So if the associated class is Project,
- # the +has_and_belongs_to_many+ association will use "project_id" as the default <tt>:association_foreign_key</tt>.
+ # Specify the foreign key used for the association on the receiving side of the association.
+ # By default this is guessed to be the name of the associated class in lower-case and "_id" suffixed.
+ # So if a Person class makes a +has_and_belongs_to_many+ association to Project,
+ # the association will use "project_id" as the default <tt>:association_foreign_key</tt>.
# [:conditions]
# Specify the conditions that the associated object must meet in order to be included as a +WHERE+
# SQL fragment, such as <tt>authorized = 1</tt>. Record creations from the association are scoped if a hash is used.
@@ -1196,6 +1176,8 @@ module ActiveRecord
# If true, all the associated objects are readonly through the association.
# [:validate]
# If false, don't validate the associated objects when saving the parent object. +true+ by default.
+ # [:autosave]
+ # If true, always save any loaded members and destroy members marked for destruction, when saving the parent object. Off by default.
#
# Option examples:
# has_and_belongs_to_many :projects
@@ -1207,9 +1189,6 @@ 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)
-
- add_multiple_associated_validation_callbacks(reflection.name) unless options[:validate] == false
- add_multiple_associated_save_callbacks(reflection.name)
collection_accessor_methods(reflection, HasAndBelongsToManyAssociation)
# Don't use a before_destroy callback since users' before_destroy
@@ -1243,33 +1222,30 @@ module ActiveRecord
end
def association_accessor_methods(reflection, association_proxy_class)
- ivar = "@#{reflection.name}"
-
define_method(reflection.name) do |*params|
force_reload = params.first unless params.empty?
-
- association = instance_variable_get(ivar) if instance_variable_defined?(ivar)
+ association = association_instance_get(reflection.name)
if association.nil? || force_reload
association = association_proxy_class.new(self, reflection)
retval = association.reload
if retval.nil? and association_proxy_class == BelongsToAssociation
- instance_variable_set(ivar, nil)
+ association_instance_set(reflection.name, nil)
return nil
end
- instance_variable_set(ivar, association)
+ association_instance_set(reflection.name, association)
end
association.target.nil? ? nil : association
end
define_method("loaded_#{reflection.name}?") do
- association = instance_variable_get(ivar) if instance_variable_defined?(ivar)
+ association = association_instance_get(reflection.name)
association && association.loaded?
end
define_method("#{reflection.name}=") do |new_value|
- association = instance_variable_get(ivar) if instance_variable_defined?(ivar)
+ association = association_instance_get(reflection.name)
if association.nil? || association.target != new_value
association = association_proxy_class.new(self, reflection)
@@ -1280,7 +1256,7 @@ module ActiveRecord
self.send(reflection.name, new_value)
else
association.replace(new_value)
- instance_variable_set(ivar, new_value.nil? ? nil : association)
+ association_instance_set(reflection.name, new_value.nil? ? nil : association)
end
end
@@ -1288,20 +1264,18 @@ module ActiveRecord
return if target.nil? and association_proxy_class == BelongsToAssociation
association = association_proxy_class.new(self, reflection)
association.target = target
- instance_variable_set(ivar, association)
+ association_instance_set(reflection.name, association)
end
end
def collection_reader_method(reflection, association_proxy_class)
define_method(reflection.name) do |*params|
- ivar = "@#{reflection.name}"
-
force_reload = params.first unless params.empty?
- association = instance_variable_get(ivar) if instance_variable_defined?(ivar)
+ association = association_instance_get(reflection.name)
- unless association.respond_to?(:loaded?)
+ unless association
association = association_proxy_class.new(self, reflection)
- instance_variable_set(ivar, association)
+ association_instance_set(reflection.name, association)
end
association.reload if force_reload
@@ -1336,86 +1310,15 @@ module ActiveRecord
end
end
- def add_single_associated_validation_callbacks(association_name)
- method_name = "validate_associated_records_for_#{association_name}".to_sym
- define_method(method_name) do
- association = instance_variable_get("@#{association_name}")
- if !association.nil?
- errors.add association_name unless association.target.nil? || association.valid?
- end
- end
-
- validate method_name
- end
-
- def add_multiple_associated_validation_callbacks(association_name)
- method_name = "validate_associated_records_for_#{association_name}".to_sym
- ivar = "@#{association_name}"
-
- define_method(method_name) do
- association = instance_variable_get(ivar) if instance_variable_defined?(ivar)
-
- if association.respond_to?(:loaded?)
- if new_record?
- association
- elsif association.loaded?
- association.select { |record| record.new_record? }
- else
- association.target.select { |record| record.new_record? }
- end.each do |record|
- errors.add association_name unless record.valid?
- end
- end
- end
-
- validate method_name
- end
-
- def add_multiple_associated_save_callbacks(association_name)
- ivar = "@#{association_name}"
-
- method_name = "before_save_associated_records_for_#{association_name}".to_sym
- define_method(method_name) do
- @new_record_before_save = new_record?
- true
- end
- before_save method_name
-
- method_name = "after_create_or_update_associated_records_for_#{association_name}".to_sym
- define_method(method_name) do
- association = instance_variable_get(ivar) if instance_variable_defined?(ivar)
-
- records_to_save = if @new_record_before_save
- association
- elsif association.respond_to?(:loaded?) && association.loaded?
- association.select { |record| record.new_record? }
- elsif association.respond_to?(:loaded?) && !association.loaded?
- association.target.select { |record| record.new_record? }
- else
- []
- end
- records_to_save.each { |record| association.send(:insert_record, record) } unless records_to_save.blank?
-
- # reconstruct the SQL queries now that we know the owner's id
- association.send(:construct_sql) if association.respond_to?(:construct_sql)
- end
-
- # Doesn't use after_save as that would save associations added in after_create/after_update twice
- after_create method_name
- after_update method_name
- end
-
def association_constructor_method(constructor, reflection, association_proxy_class)
define_method("#{constructor}_#{reflection.name}") do |*params|
- ivar = "@#{reflection.name}"
-
attributees = params.first unless params.empty?
replace_existing = params[1].nil? ? true : params[1]
- association = instance_variable_get(ivar) if instance_variable_defined?(ivar)
+ association = association_instance_get(reflection.name)
- if association.nil?
+ unless association
association = association_proxy_class.new(self, reflection)
- instance_variable_set(ivar, association)
+ association_instance_set(reflection.name, association)
end
if association_proxy_class == HasOneAssociation
@@ -1627,6 +1530,10 @@ module ActiveRecord
options[:extend] = create_extension_modules(association_id, extension, options[:extend])
reflection = create_reflection(:has_and_belongs_to_many, association_id, options, self)
+
+ if reflection.association_foreign_key == reflection.primary_key_name
+ raise HasAndBelongsToManyAssociationForeignKeyNeeded.new(reflection)
+ end
reflection.options[:join_table] ||= join_table_name(undecorated_table_name(self.to_s), undecorated_table_name(reflection.class_name))
@@ -1949,9 +1856,10 @@ module ActiveRecord
def construct(parent, associations, joins, row)
case associations
when Symbol, String
- while (join = joins.shift).reflection.name.to_s != associations.to_s
- raise ConfigurationError, "Not Enough Associations" if joins.empty?
- end
+ join = joins.detect{|j| j.reflection.name.to_s == associations.to_s && j.parent_table_name == parent.class.table_name }
+ raise(ConfigurationError, "No such association") if join.nil?
+
+ joins.delete(join)
construct_association(parent, join, row)
when Array
associations.each do |association|
@@ -1959,7 +1867,11 @@ module ActiveRecord
end
when Hash
associations.keys.sort{|a,b|a.to_s<=>b.to_s}.each do |name|
- association = construct_association(parent, joins.shift, row)
+ join = joins.detect{|j| j.reflection.name.to_s == name.to_s && j.parent_table_name == parent.class.table_name }
+ raise(ConfigurationError, "No such association") if join.nil?
+
+ association = construct_association(parent, join, row)
+ joins.delete(join)
construct(association, associations[name], joins, row) if association
end
else
diff --git a/activerecord/lib/active_record/associations/association_collection.rb b/activerecord/lib/active_record/associations/association_collection.rb
index 0fefec1216..3aef1b21e9 100644
--- a/activerecord/lib/active_record/associations/association_collection.rb
+++ b/activerecord/lib/active_record/associations/association_collection.rb
@@ -60,7 +60,7 @@ module ActiveRecord
@reflection.klass.find(*args)
end
end
-
+
# Fetches the first one using SQL if possible.
def first(*args)
if fetch_first_or_last_using_find?(args)
@@ -143,6 +143,8 @@ module ActiveRecord
end
# Remove all records from this association
+ #
+ # See delete for more info.
def delete_all
load_target
delete(@target)
@@ -186,7 +188,6 @@ module ActiveRecord
end
end
-
# Removes +records+ from this association calling +before_remove+ and
# +after_remove+ callbacks.
#
@@ -195,22 +196,25 @@ 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)
- records = flatten_deeper(records)
- records.each { |record| raise_on_type_mismatch(record) }
-
- transaction do
- records.each { |record| callback(:before_remove, record) }
-
- old_records = records.reject {|r| r.new_record? }
+ remove_records(records) do |records, old_records|
delete_records(old_records) if old_records.any?
-
- records.each do |record|
- @target.delete(record)
- callback(:after_remove, record)
- end
+ records.each { |record| @target.delete(record) }
end
end
+ # Destroy +records+ and remove them from this association calling
+ # +before_remove+ and +after_remove+ callbacks.
+ #
+ # Note that this method will _always_ remove records from the database
+ # ignoring the +:dependent+ option.
+ def destroy(*records)
+ 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
return self if length.zero? # forces load_target if it hasn't happened already
@@ -223,15 +227,16 @@ module ActiveRecord
self
end
-
- def destroy_all
- transaction do
- each { |record| record.destroy }
- end
+ # Destory all the records from this association.
+ #
+ # See destroy for more info.
+ def destroy_all
+ load_target
+ destroy(@target)
reset_target!
end
-
+
def create(attrs = {})
if attrs.is_a?(Array)
attrs.collect { |attr| create(attr) }
@@ -431,6 +436,18 @@ module ActiveRecord
record
end
+ def remove_records(*records)
+ records = flatten_deeper(records)
+ records.each { |record| raise_on_type_mismatch(record) }
+
+ transaction do
+ records.each { |record| callback(:before_remove, record) }
+ old_records = records.reject { |r| r.new_record? }
+ yield(records, old_records)
+ records.each { |record| callback(:after_remove, record) }
+ end
+ end
+
def callback(method, record)
callbacks_for(method).each do |callback|
ActiveSupport::Callbacks::Callback.new(method, callback, record).call(@owner, record)
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 a5cc3bf091..af9ce3dfb2 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
@@ -28,12 +28,12 @@ module ActiveRecord
load_target.size
end
- def insert_record(record, force=true)
+ def insert_record(record, force = true, validate = true)
if record.new_record?
if force
record.save!
else
- return false unless record.save
+ return false unless record.save(validate)
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 3348079e9d..a2cbabfe0c 100644
--- a/activerecord/lib/active_record/associations/has_many_association.rb
+++ b/activerecord/lib/active_record/associations/has_many_association.rb
@@ -56,9 +56,9 @@ module ActiveRecord
"#{@reflection.name}_count"
end
- def insert_record(record)
+ def insert_record(record, force = false, validate = true)
set_belongs_to_association_for(record)
- record.save
+ force ? record.save! : record.save(validate)
end
# Deletes the records according to the <tt>:dependent</tt> option.
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 2eeeb28d1f..1c091e7d5a 100644
--- a/activerecord/lib/active_record/associations/has_many_through_association.rb
+++ b/activerecord/lib/active_record/associations/has_many_through_association.rb
@@ -23,8 +23,8 @@ module ActiveRecord
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 less SELECT query if you use length.
+ # 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?
@@ -47,12 +47,12 @@ module ActiveRecord
options[:include] = @reflection.source_reflection.options[:include] if options[:include].nil?
end
- def insert_record(record, force=true)
+ def insert_record(record, force = true, validate = true)
if record.new_record?
if force
record.save!
else
- return false unless record.save
+ return false unless record.save(validate)
end
end
through_reflection = @reflection.through_reflection
@@ -150,7 +150,7 @@ module ActiveRecord
end
else
reflection_primary_key = @reflection.source_reflection.primary_key_name
- source_primary_key = @reflection.klass.primary_key
+ source_primary_key = @reflection.through_reflection.klass.primary_key
if @reflection.source_reflection.options[:as]
polymorphic_join = "AND %s.%s = %s" % [
@reflection.quoted_table_name, "#{@reflection.source_reflection.options[:as]}_type",
diff --git a/activerecord/lib/active_record/associations/has_one_association.rb b/activerecord/lib/active_record/associations/has_one_association.rb
index 960323004d..b92cbbdeab 100644
--- a/activerecord/lib/active_record/associations/has_one_association.rb
+++ b/activerecord/lib/active_record/associations/has_one_association.rb
@@ -29,8 +29,17 @@ module ActiveRecord
unless @target.nil? || @target == obj
if dependent? && !dont_save
- @target.destroy unless @target.new_record?
- @owner.clear_association_cache
+ case @reflection.options[:dependent]
+ when :delete
+ @target.delete unless @target.new_record?
+ @owner.clear_association_cache
+ when :destroy
+ @target.destroy unless @target.new_record?
+ @owner.clear_association_cache
+ when :nullify
+ @target[@reflection.primary_key_name] = nil
+ @target.save unless @owner.new_record? || @target.new_record?
+ end
else
@target[@reflection.primary_key_name] = nil
@target.save unless @owner.new_record? || @target.new_record?
diff --git a/activerecord/lib/active_record/attribute_methods.rb b/activerecord/lib/active_record/attribute_methods.rb
index 177d156834..3ffc48941c 100644
--- a/activerecord/lib/active_record/attribute_methods.rb
+++ b/activerecord/lib/active_record/attribute_methods.rb
@@ -324,6 +324,7 @@ module ActiveRecord
if Numeric === value || value !~ /[^0-9]/
!value.to_i.zero?
else
+ return false if ActiveRecord::ConnectionAdapters::Column::FALSE_VALUES.include?(value)
!value.blank?
end
elsif column.number?
diff --git a/activerecord/lib/active_record/autosave_association.rb b/activerecord/lib/active_record/autosave_association.rb
new file mode 100644
index 0000000000..741aa2acbe
--- /dev/null
+++ b/activerecord/lib/active_record/autosave_association.rb
@@ -0,0 +1,349 @@
+module ActiveRecord
+ # AutosaveAssociation is a module that takes care of automatically saving
+ # your associations when the parent is saved. In addition to saving, it
+ # also destroys any associations that were marked for destruction.
+ # (See mark_for_destruction and marked_for_destruction?)
+ #
+ # Saving of the parent, its associations, and the destruction of marked
+ # associations, all happen inside 1 transaction. This should never leave the
+ # database in an inconsistent state after, for instance, mass assigning
+ # attributes and saving them.
+ #
+ # If validations for any of the associations fail, their error messages will
+ # be applied to the parent.
+ #
+ # Note that it also means that associations marked for destruction won't
+ # be destroyed directly. They will however still be marked for destruction.
+ #
+ # === One-to-one Example
+ #
+ # Consider a Post model with one Author:
+ #
+ # class Post
+ # has_one :author, :autosave => true
+ # end
+ #
+ # Saving changes to the parent and its associated model can now be performed
+ # automatically _and_ atomically:
+ #
+ # post = Post.find(1)
+ # post.title # => "The current global position of migrating ducks"
+ # post.author.name # => "alloy"
+ #
+ # post.title = "On the migration of ducks"
+ # post.author.name = "Eloy Duran"
+ #
+ # post.save
+ # post.reload
+ # post.title # => "On the migration of ducks"
+ # post.author.name # => "Eloy Duran"
+ #
+ # Destroying an associated model, as part of the parent's save action, is as
+ # simple as marking it for destruction:
+ #
+ # post.author.mark_for_destruction
+ # post.author.marked_for_destruction? # => true
+ #
+ # Note that the model is _not_ yet removed from the database:
+ # id = post.author.id
+ # Author.find_by_id(id).nil? # => false
+ #
+ # post.save
+ # post.reload.author # => nil
+ #
+ # Now it _is_ removed from the database:
+ # Author.find_by_id(id).nil? # => true
+ #
+ # === One-to-many Example
+ #
+ # Consider a Post model with many Comments:
+ #
+ # class Post
+ # has_many :comments, :autosave => true
+ # end
+ #
+ # Saving changes to the parent and its associated model can now be performed
+ # automatically _and_ atomically:
+ #
+ # post = Post.find(1)
+ # post.title # => "The current global position of migrating ducks"
+ # post.comments.first.body # => "Wow, awesome info thanks!"
+ # post.comments.last.body # => "Actually, your article should be named differently."
+ #
+ # post.title = "On the migration of ducks"
+ # post.comments.last.body = "Actually, your article should be named differently. [UPDATED]: You are right, thanks."
+ #
+ # post.save
+ # post.reload
+ # post.title # => "On the migration of ducks"
+ # post.comments.last.body # => "Actually, your article should be named differently. [UPDATED]: You are right, thanks."
+ #
+ # Destroying one of the associated models members, as part of the parent's
+ # save action, is as simple as marking it for destruction:
+ #
+ # post.comments.last.mark_for_destruction
+ # post.comments.last.marked_for_destruction? # => true
+ # post.comments.length # => 2
+ #
+ # Note that the model is _not_ yet removed from the database:
+ # id = post.comments.last.id
+ # Comment.find_by_id(id).nil? # => false
+ #
+ # post.save
+ # post.reload.comments.length # => 1
+ #
+ # Now it _is_ removed from the database:
+ # Comment.find_by_id(id).nil? # => true
+ #
+ # === Validation
+ #
+ # Validation is performed on the parent as usual, but also on all autosave
+ # enabled associations. If any of the associations fail validation, its
+ # error messages will be applied on the parents errors object and validation
+ # of the parent will fail.
+ #
+ # Consider a Post model with Author which validates the presence of its name
+ # attribute:
+ #
+ # class Post
+ # has_one :author, :autosave => true
+ # end
+ #
+ # class Author
+ # validates_presence_of :name
+ # end
+ #
+ # post = Post.find(1)
+ # post.author.name = ''
+ # post.save # => false
+ # post.errors # => #<ActiveRecord::Errors:0x174498c @errors={"author_name"=>["can't be blank"]}, @base=#<Post ...>>
+ #
+ # No validations will be performed on the associated models when validations
+ # are skipped for the parent:
+ #
+ # post = Post.find(1)
+ # post.author.name = ''
+ # post.save(false) # => true
+ module AutosaveAssociation
+ ASSOCIATION_TYPES = %w{ has_one belongs_to has_many has_and_belongs_to_many }
+
+ def self.included(base)
+ base.class_eval do
+ base.extend(ClassMethods)
+ alias_method_chain :reload, :autosave_associations
+
+ ASSOCIATION_TYPES.each do |type|
+ base.send("valid_keys_for_#{type}_association") << :autosave
+ end
+ end
+ end
+
+ module ClassMethods
+ private
+
+ # def belongs_to(name, options = {})
+ # super
+ # add_autosave_association_callbacks(reflect_on_association(name))
+ # end
+ ASSOCIATION_TYPES.each do |type|
+ module_eval %{
+ def #{type}(name, options = {})
+ super
+ add_autosave_association_callbacks(reflect_on_association(name))
+ end
+ }
+ end
+
+ # Adds a validate and save callback for the association as specified by
+ # the +reflection+.
+ def add_autosave_association_callbacks(reflection)
+ save_method = "autosave_associated_records_for_#{reflection.name}"
+ validation_method = "validate_associated_records_for_#{reflection.name}"
+ validate validation_method
+
+ case reflection.macro
+ when :has_many, :has_and_belongs_to_many
+ before_save :before_save_collection_association
+
+ define_method(save_method) { save_collection_association(reflection) }
+ # Doesn't use after_save as that would save associations added in after_create/after_update twice
+ after_create save_method
+ after_update save_method
+
+ define_method(validation_method) { validate_collection_association(reflection) }
+ else
+ case reflection.macro
+ when :has_one
+ define_method(save_method) { save_has_one_association(reflection) }
+ after_save save_method
+ when :belongs_to
+ define_method(save_method) { save_belongs_to_association(reflection) }
+ before_save save_method
+ end
+ define_method(validation_method) { validate_single_association(reflection) }
+ end
+ end
+ end
+
+ # Reloads the attributes of the object as usual and removes a mark for destruction.
+ def reload_with_autosave_associations(options = nil)
+ @marked_for_destruction = false
+ reload_without_autosave_associations(options)
+ end
+
+ # Marks this record to be destroyed as part of the parents save transaction.
+ # This does _not_ actually destroy the record yet, rather it will be destroyed when <tt>parent.save</tt> is called.
+ #
+ # Only useful if the <tt>:autosave</tt> option on the parent is enabled for this associated model.
+ def mark_for_destruction
+ @marked_for_destruction = true
+ end
+
+ # Returns whether or not this record will be destroyed as part of the parents save transaction.
+ #
+ # Only useful if the <tt>:autosave</tt> option on the parent is enabled for this associated model.
+ def marked_for_destruction?
+ @marked_for_destruction
+ end
+
+ private
+
+ # Returns the record for an association collection that should be validated
+ # or saved. If +autosave+ is +false+ only new records will be returned,
+ # unless the parent is/was a new record itself.
+ def associated_records_to_validate_or_save(association, new_record, autosave)
+ if new_record
+ association
+ elsif association.loaded?
+ autosave ? association : association.select { |record| record.new_record? }
+ else
+ autosave ? association.target : association.target.select { |record| record.new_record? }
+ end
+ end
+
+ # Validate the association if <tt>:validate</tt> or <tt>:autosave</tt> is
+ # turned on for the association specified by +reflection+.
+ def validate_single_association(reflection)
+ if reflection.options[:validate] == true || reflection.options[:autosave] == true
+ if (association = association_instance_get(reflection.name)) && !association.target.nil?
+ association_valid?(reflection, association)
+ end
+ end
+ end
+
+ # Validate the associated records if <tt>:validate</tt> or
+ # <tt>:autosave</tt> is turned on for the association specified by
+ # +reflection+.
+ def validate_collection_association(reflection)
+ if reflection.options[:validate] != false && association = association_instance_get(reflection.name)
+ if records = associated_records_to_validate_or_save(association, new_record?, reflection.options[:autosave])
+ records.each { |record| association_valid?(reflection, record) }
+ end
+ end
+ end
+
+ # Returns whether or not the association is valid and applies any errors to
+ # the parent, <tt>self</tt>, if it wasn't. Skips any <tt>:autosave</tt>
+ # enabled records if they're marked_for_destruction?.
+ def association_valid?(reflection, association)
+ unless valid = association.valid?
+ if reflection.options[:autosave]
+ unless association.marked_for_destruction?
+ association.errors.each do |attribute, message|
+ attribute = "#{reflection.name}_#{attribute}"
+ errors.add(attribute, message) unless errors.on(attribute)
+ end
+ end
+ else
+ errors.add(reflection.name)
+ end
+ end
+ valid
+ end
+
+ # Is used as a before_save callback to check while saving a collection
+ # association whether or not the parent was a new record before saving.
+ def before_save_collection_association
+ @new_record_before_save = new_record?
+ true
+ end
+
+ # Saves any new associated records, or all loaded autosave associations if
+ # <tt>:autosave</tt> is enabled on the association.
+ #
+ # In addition, it destroys all children that were marked for destruction
+ # with mark_for_destruction.
+ #
+ # This all happens inside a transaction, _if_ the Transactions module is included into
+ # ActiveRecord::Base after the AutosaveAssociation module, which it does by default.
+ def save_collection_association(reflection)
+ if association = association_instance_get(reflection.name)
+ autosave = reflection.options[:autosave]
+
+ if records = associated_records_to_validate_or_save(association, @new_record_before_save, autosave)
+ records.each do |record|
+ if autosave && record.marked_for_destruction?
+ association.destroy(record)
+ elsif @new_record_before_save || record.new_record?
+ if autosave
+ association.send(:insert_record, record, false, false)
+ else
+ association.send(:insert_record, record)
+ end
+ elsif autosave
+ record.save(false)
+ end
+ end
+ end
+
+ # reconstruct the SQL queries now that we know the owner's id
+ association.send(:construct_sql) if association.respond_to?(:construct_sql)
+ end
+ end
+
+ # Saves the associated record if it's new or <tt>:autosave</tt> is enabled
+ # on the association.
+ #
+ # In addition, it will destroy the association if it was marked for
+ # destruction with mark_for_destruction.
+ #
+ # This all happens inside a transaction, _if_ the Transactions module is included into
+ # ActiveRecord::Base after the AutosaveAssociation module, which it does by default.
+ def save_has_one_association(reflection)
+ if (association = association_instance_get(reflection.name)) && !association.target.nil?
+ if reflection.options[:autosave] && association.marked_for_destruction?
+ association.destroy
+ elsif new_record? || association.new_record? || association[reflection.primary_key_name] != id || reflection.options[:autosave]
+ association[reflection.primary_key_name] = id
+ association.save(false)
+ end
+ end
+ end
+
+ # Saves the associated record if it's new or <tt>:autosave</tt> is enabled
+ # on the association.
+ #
+ # In addition, it will destroy the association if it was marked for
+ # destruction with mark_for_destruction.
+ #
+ # This all happens inside a transaction, _if_ the Transactions module is included into
+ # ActiveRecord::Base after the AutosaveAssociation module, which it does by default.
+ def save_belongs_to_association(reflection)
+ if association = association_instance_get(reflection.name)
+ if reflection.options[:autosave] && association.marked_for_destruction?
+ association.destroy
+ else
+ association.save(false) if association.new_record? || reflection.options[:autosave]
+
+ if association.updated?
+ self[reflection.primary_key_name] = association.id
+ # TODO: Removing this code doesn't seem to matter…
+ if reflection.options[:polymorphic]
+ self[reflection.options[:foreign_type]] = association.class.base_class.name.to_s
+ end
+ end
+ end
+ end
+ end
+ end
+end \ No newline at end of file
diff --git a/activerecord/lib/active_record/base.rb b/activerecord/lib/active_record/base.rb
index 0efccb66ee..9943a7014a 100755
--- a/activerecord/lib/active_record/base.rb
+++ b/activerecord/lib/active_record/base.rb
@@ -416,7 +416,7 @@ module ActiveRecord #:nodoc:
end
@@subclasses = {}
-
+
##
# :singleton-method:
# Contains the database configuration - as is typically stored in config/database.yml -
@@ -661,9 +661,8 @@ module ActiveRecord #:nodoc:
connection.select_all(sanitize_sql(sql), "#{name} Load").collect! { |record| instantiate(record) }
end
-
# Returns true if a record exists in the table that matches the +id+ or
- # conditions given, or false otherwise. The argument can take four forms:
+ # conditions given, or false otherwise. The argument can take five forms:
#
# * Integer - Finds the record with this primary key.
# * String - Finds the record with a primary key corresponding to this
@@ -672,6 +671,7 @@ module ActiveRecord #:nodoc:
# (such as <tt>['color = ?', 'red']</tt>).
# * Hash - Finds the record that matches these +find+-style conditions
# (such as <tt>{:color => 'red'}</tt>).
+ # * No args - Returns false if the table is empty, true otherwise.
#
# For more information about specifying conditions as a Hash or Array,
# see the Conditions section in the introduction to ActiveRecord::Base.
@@ -685,7 +685,8 @@ module ActiveRecord #:nodoc:
# Person.exists?('5')
# Person.exists?(:name => "David")
# Person.exists?(['name LIKE ?', "%#{query}%"])
- def exists?(id_or_conditions)
+ # Person.exists?
+ def exists?(id_or_conditions = {})
connection.select_all(
construct_finder_sql(
:select => "#{quoted_table_name}.#{primary_key}",
@@ -735,12 +736,12 @@ module ActiveRecord #:nodoc:
# ==== Parameters
#
# * +id+ - This should be the id or an array of ids to be updated.
- # * +attributes+ - This should be a Hash of attributes to be set on the object, or an array of Hashes.
+ # * +attributes+ - This should be a hash of attributes to be set on the object, or an array of hashes.
#
# ==== Examples
#
# # Updating one record:
- # Person.update(15, { :user_name => 'Samuel', :group => 'expert' })
+ # Person.update(15, :user_name => 'Samuel', :group => 'expert')
#
# # Updating multiple records:
# people = { 1 => { "first_name" => "David" }, 2 => { "first_name" => "Jeremy" } }
@@ -809,25 +810,28 @@ module ActiveRecord #:nodoc:
# Updates all records with details given if they match a set of conditions supplied, limits and order can
# also be supplied. This method constructs a single SQL UPDATE statement and sends it straight to the
- # database. It does not instantiate the involved models and it does not trigger Active Record callbacks.
+ # database. It does not instantiate the involved models and it does not trigger Active Record callbacks
+ # or validations.
#
# ==== Parameters
#
- # * +updates+ - A string of column and value pairs that will be set on any records that match conditions. This creates the SET clause of the generated SQL.
- # * +conditions+ - An SQL fragment like "administrator = 1" or [ "user_name = ?", username ]. See conditions in the intro for more info.
+ # * +updates+ - A string, array, or hash representing the SET part of an SQL statement.
+ # * +conditions+ - A string, array, or hash representing the WHERE part of an SQL statement. See conditions in the intro.
# * +options+ - Additional options are <tt>:limit</tt> and <tt>:order</tt>, see the examples for usage.
#
# ==== Examples
#
- # # Update all billing objects with the 3 different attributes given
- # Billing.update_all( "category = 'authorized', approved = 1, author = 'David'" )
+ # # Update all customers with the given attributes
+ # Customer.update_all :wants_email => true
#
- # # Update records that match our conditions
- # Billing.update_all( "author = 'David'", "title LIKE '%Rails%'" )
+ # # Update all books with 'Rails' in their title
+ # Book.update_all "author = 'David'", "title LIKE '%Rails%'"
#
- # # Update records that match our conditions but limit it to 5 ordered by date
- # Billing.update_all( "author = 'David'", "title LIKE '%Rails%'",
- # :order => 'created_at', :limit => 5 )
+ # # Update all avatars migrated more than a week ago
+ # Avatar.update_all ['migrated_at = ?, Time.now.utc], ['migrated_at > ?', 1.week.ago]
+ #
+ # # Update all books that match our conditions, but limit it to 5 ordered by date
+ # Book.update_all "author = 'David'", "title LIKE '%Rails%'", :order => 'created_at', :limit => 5
def update_all(updates, conditions = nil, options = {})
sql = "UPDATE #{quoted_table_name} SET #{sanitize_sql_for_assignment(updates)} "
@@ -884,7 +888,8 @@ module ActiveRecord #:nodoc:
# Deletes the records matching +conditions+ without instantiating the records first, and hence not
# calling the +destroy+ method nor invoking callbacks. This is a single SQL DELETE statement that
# goes straight to the database, much more efficient than +destroy_all+. Be careful with relations
- # though, in particular <tt>:dependent</tt> rules defined on associations are not honored.
+ # though, in particular <tt>:dependent</tt> rules defined on associations are not honored. Returns
+ # the number of rows affected.
#
# ==== Parameters
#
@@ -1000,7 +1005,6 @@ module ActiveRecord #:nodoc:
update_counters(id, counter_name => -1)
end
-
# Attributes named in this macro are protected from mass-assignment,
# such as <tt>new(attributes)</tt>,
# <tt>update_attributes(attributes)</tt>, or
@@ -1101,7 +1105,6 @@ module ActiveRecord #:nodoc:
read_inheritable_attribute(:attr_serialized) or write_inheritable_attribute(:attr_serialized, {})
end
-
# Guesses the table name (in forced lower-case) based on the name of the class in the inheritance hierarchy descending
# directly from ActiveRecord::Base. So if the hierarchy looks like: Reply < Message < ActiveRecord::Base, then Message is used
# to guess the table name even when called on Reply. The rules used to do the guess are handled by the Inflector class
@@ -1344,7 +1347,7 @@ module ActiveRecord #:nodoc:
subclasses.each { |klass| klass.reset_inheritable_attributes; klass.reset_column_information }
end
- def self_and_descendents_from_active_record#nodoc:
+ def self_and_descendants_from_active_record#nodoc:
klass = self
classes = [klass]
while klass != klass.base_class
@@ -1364,7 +1367,7 @@ module ActiveRecord #:nodoc:
# module now.
# Specify +options+ with additional translating options.
def human_attribute_name(attribute_key_name, options = {})
- defaults = self_and_descendents_from_active_record.map do |klass|
+ defaults = self_and_descendants_from_active_record.map do |klass|
:"#{klass.name.underscore}.#{attribute_key_name}"
end
defaults << options[:default] if options[:default]
@@ -1379,7 +1382,7 @@ module ActiveRecord #:nodoc:
# Default scope of the translation is activerecord.models
# Specify +options+ with additional translating options.
def human_name(options = {})
- defaults = self_and_descendents_from_active_record.map do |klass|
+ defaults = self_and_descendants_from_active_record.map do |klass|
:"#{klass.name.underscore}"
end
defaults << self.name.humanize
@@ -1414,7 +1417,6 @@ module ActiveRecord #:nodoc:
end
end
-
def quote_value(value, column = nil) #:nodoc:
connection.quote(value,column)
end
@@ -1483,7 +1485,7 @@ module ActiveRecord #:nodoc:
elsif match = DynamicScopeMatch.match(method_id)
return true if all_attributes_exists?(match.attribute_names)
end
-
+
super
end
@@ -1534,7 +1536,7 @@ module ActiveRecord #:nodoc:
end
def reverse_sql_order(order_query)
- reversed_query = order_query.split(/,/).each { |s|
+ reversed_query = order_query.to_s.split(/,/).each { |s|
if s.match(/\s(asc|ASC)$/)
s.gsub!(/\s(asc|ASC)$/, ' DESC')
elsif s.match(/\s(desc|DESC)$/)
@@ -1687,7 +1689,7 @@ module ActiveRecord #:nodoc:
def construct_finder_sql(options)
scope = scope(:find)
sql = "SELECT #{options[:select] || (scope && scope[:select]) || default_select(options[:joins] || (scope && scope[:joins]))} "
- sql << "FROM #{(scope && scope[:from]) || options[:from] || quoted_table_name} "
+ sql << "FROM #{options[:from] || (scope && scope[:from]) || quoted_table_name} "
add_joins!(sql, options[:joins], scope)
add_conditions!(sql, options[:conditions], scope)
@@ -1742,7 +1744,9 @@ module ActiveRecord #:nodoc:
scoped_order = scope[:order] if scope
if order
sql << " ORDER BY #{order}"
- sql << ", #{scoped_order}" if scoped_order
+ if scoped_order && scoped_order != order
+ sql << ", #{scoped_order}"
+ end
else
sql << " ORDER BY #{scoped_order}" if scoped_order
end
@@ -1751,12 +1755,12 @@ module ActiveRecord #:nodoc:
def add_group!(sql, group, having, scope = :auto)
if group
sql << " GROUP BY #{group}"
- sql << " HAVING #{having}" if having
+ sql << " HAVING #{sanitize_sql_for_conditions(having)}" if having
else
scope = scope(:find) if :auto == scope
if scope && (scoped_group = scope[:group])
sql << " GROUP BY #{scoped_group}"
- sql << " HAVING #{scoped_having}" if (scoped_having = scope[:having])
+ sql << " HAVING #{sanitize_sql_for_conditions(scope[:having])}" if scope[:having]
end
end
end
@@ -1990,12 +1994,16 @@ module ActiveRecord #:nodoc:
attribute_names.all? { |name| column_methods_hash.include?(name.to_sym) }
end
- def attribute_condition(argument)
+ def attribute_condition(quoted_column_name, argument)
case argument
- when nil then "IS ?"
- when Array, ActiveRecord::Associations::AssociationCollection, ActiveRecord::NamedScope::Scope then "IN (?)"
- when Range then "BETWEEN ? AND ?"
- else "= ?"
+ when nil then "#{quoted_column_name} IS ?"
+ when Array, ActiveRecord::Associations::AssociationCollection, ActiveRecord::NamedScope::Scope then "#{quoted_column_name} IN (?)"
+ when Range then if argument.exclude_end?
+ "#{quoted_column_name} >= ? AND #{quoted_column_name} < ?"
+ else
+ "#{quoted_column_name} BETWEEN ? AND ?"
+ end
+ else "#{quoted_column_name} = ?"
end
end
@@ -2007,7 +2015,6 @@ module ActiveRecord #:nodoc:
end
end
-
# Defines an "attribute" method (like +inheritance_column+ or
# +table_name+). A new (class) method will be created with the
# given name. If a value is specified, the new method will
@@ -2104,7 +2111,7 @@ module ActiveRecord #:nodoc:
end
# Merge scopings
- if action == :merge && current_scoped_methods
+ if [:merge, :reverse_merge].include?(action) && current_scoped_methods
method_scoping = current_scoped_methods.inject(method_scoping) do |hash, (method, params)|
case hash[method]
when Hash
@@ -2126,7 +2133,11 @@ module ActiveRecord #:nodoc:
end
end
else
- hash[method] = hash[method].merge(params)
+ if action == :reverse_merge
+ hash[method] = hash[method].merge(params)
+ else
+ hash[method] = params.merge(hash[method])
+ end
end
else
hash[method] = params
@@ -2136,7 +2147,6 @@ module ActiveRecord #:nodoc:
end
self.scoped_methods << method_scoping
-
begin
yield
ensure
@@ -2167,7 +2177,7 @@ module ActiveRecord #:nodoc:
# Test whether the given method and optional key are scoped.
def scoped?(method, key = nil) #:nodoc:
if current_scoped_methods && (scope = current_scoped_methods[method])
- !key || scope.has_key?(key)
+ !key || !scope[key].nil?
end
end
@@ -2305,7 +2315,7 @@ module ActiveRecord #:nodoc:
table_name = connection.quote_table_name(table_name)
end
- "#{table_name}.#{connection.quote_column_name(attr)} #{attribute_condition(value)}"
+ attribute_condition("#{table_name}.#{connection.quote_column_name(attr)}", value)
else
sanitize_sql_hash_for_conditions(value, connection.quote_table_name(attr.to_s))
end
@@ -2742,7 +2752,6 @@ module ActiveRecord #:nodoc:
assign_multiparameter_attributes(multi_parameter_attributes)
end
-
# Returns a hash of all the attributes with their names as keys and the values of the attributes as values.
def attributes
self.attribute_names.inject({}) do |attrs, name|
@@ -3136,7 +3145,12 @@ module ActiveRecord #:nodoc:
include Dirty
include Callbacks, Observing, Timestamp
include Associations, AssociationPreload, NamedScope
- include Aggregations, Transactions, Reflection, Calculations, Serialization
+
+ # AutosaveAssociation needs to be included before Transactions, because we want
+ # #save_with_autosave_associations to be wrapped inside a transaction.
+ include AutosaveAssociation, NestedAttributes
+
+ include Aggregations, Transactions, Reflection, Batches, Calculations, Serialization
end
end
diff --git a/activerecord/lib/active_record/batches.rb b/activerecord/lib/active_record/batches.rb
new file mode 100644
index 0000000000..5a6cecd4ad
--- /dev/null
+++ b/activerecord/lib/active_record/batches.rb
@@ -0,0 +1,81 @@
+module ActiveRecord
+ module Batches # :nodoc:
+ def self.included(base)
+ base.extend(ClassMethods)
+ end
+
+ # When processing large numbers of records, it's often a good idea to do
+ # so in batches to prevent memory ballooning.
+ module ClassMethods
+ # Yields each record that was found by the find +options+. The find is
+ # performed by find_in_batches with a batch size of 1000 (or as
+ # specified by the <tt>:batch_size</tt> option).
+ #
+ # Example:
+ #
+ # Person.find_each(:conditions => "age > 21") do |person|
+ # person.party_all_night!
+ # end
+ #
+ # Note: This method is only intended to use for batch processing of
+ # large amounts of records that wouldn't fit in memory all at once. If
+ # you just need to loop over less than 1000 records, it's probably
+ # better just to use the regular find methods.
+ def find_each(options = {})
+ find_in_batches(options) do |records|
+ records.each { |record| yield record }
+ end
+
+ self
+ end
+
+ # Yields each batch of records that was found by the find +options+ as
+ # an array. The size of each batch is set by the <tt>:batch_size</tt>
+ # option; the default is 1000.
+ #
+ # You can control the starting point for the batch processing by
+ # supplying the <tt>:start</tt> option. This is especially useful if you
+ # want multiple workers dealing with the same processing queue. You can
+ # make worker 1 handle all the records between id 0 and 10,000 and
+ # worker 2 handle from 10,000 and beyond (by setting the <tt>:start</tt>
+ # option on that worker).
+ #
+ # It's not possible to set the order. That is automatically set to
+ # ascending on the primary key ("id ASC") to make the batch ordering
+ # work. This also mean that this method only works with integer-based
+ # primary keys. You can't set the limit either, that's used to control
+ # the the batch sizes.
+ #
+ # Example:
+ #
+ # Person.find_in_batches(:conditions => "age > 21") do |group|
+ # sleep(50) # Make sure it doesn't get too crowded in there!
+ # group.each { |person| person.party_all_night! }
+ # end
+ def find_in_batches(options = {})
+ raise "You can't specify an order, it's forced to be #{batch_order}" if options[:order]
+ raise "You can't specify a limit, it's forced to be the batch_size" if options[:limit]
+
+ start = options.delete(:start).to_i
+ batch_size = options.delete(:batch_size) || 1000
+
+ with_scope(:find => options.merge(:order => batch_order, :limit => batch_size)) do
+ records = find(:all, :conditions => [ "#{table_name}.#{primary_key} >= ?", start ])
+
+ while records.any?
+ yield records
+
+ break if records.size < batch_size
+ records = find(:all, :conditions => [ "#{table_name}.#{primary_key} > ?", records.last.id ])
+ end
+ end
+ end
+
+
+ private
+ def batch_order
+ "#{table_name}.#{primary_key} ASC"
+ end
+ end
+ end
+end \ No newline at end of file
diff --git a/activerecord/lib/active_record/calculations.rb b/activerecord/lib/active_record/calculations.rb
index b239c03284..f077818d3b 100644
--- a/activerecord/lib/active_record/calculations.rb
+++ b/activerecord/lib/active_record/calculations.rb
@@ -141,22 +141,30 @@ module ActiveRecord
def construct_count_options_from_args(*args)
options = {}
column_name = :all
-
+
# We need to handle
# count()
# count(:column_name=:all)
# count(options={})
# count(column_name=:all, options={})
+ # selects specified by scopes
case args.size
+ when 0
+ column_name = scope(:find)[:select] if scope(:find)
when 1
- args[0].is_a?(Hash) ? options = args[0] : column_name = args[0]
+ if args[0].is_a?(Hash)
+ column_name = scope(:find)[:select] if scope(:find)
+ options = args[0]
+ else
+ column_name = args[0]
+ end
when 2
column_name, options = args
else
raise ArgumentError, "Unexpected parameters passed to count(): #{args.inspect}"
- end if args.size > 0
-
- [column_name, options]
+ end
+
+ [column_name || :all, options]
end
def construct_calculation_sql(operation, column_name, options) #:nodoc:
@@ -214,13 +222,15 @@ module ActiveRecord
end
if options[:group] && options[:having]
+ having = sanitize_sql_for_conditions(options[:having])
+
# FrontBase requires identifiers in the HAVING clause and chokes on function calls
if connection.adapter_name == 'FrontBase'
- options[:having].downcase!
- options[:having].gsub!(/#{operation}\s*\(\s*#{column_name}\s*\)/, aggregate_alias)
+ having.downcase!
+ having.gsub!(/#{operation}\s*\(\s*#{column_name}\s*\)/, aggregate_alias)
end
- sql << " HAVING #{options[:having]} "
+ sql << " HAVING #{having} "
end
sql << " ORDER BY #{options[:order]} " if options[:order]
diff --git a/activerecord/lib/active_record/callbacks.rb b/activerecord/lib/active_record/callbacks.rb
index 88958f4583..e375037b5b 100644
--- a/activerecord/lib/active_record/callbacks.rb
+++ b/activerecord/lib/active_record/callbacks.rb
@@ -104,6 +104,37 @@ module ActiveRecord
# The callback objects have methods named after the callback called with the record as the only parameter, such as:
#
# class BankAccount < ActiveRecord::Base
+ # before_save EncryptionWrapper.new
+ # after_save EncryptionWrapper.new
+ # after_initialize EncryptionWrapper.new
+ # end
+ #
+ # class EncryptionWrapper
+ # def before_save(record)
+ # record.credit_card_number = encrypt(record.credit_card_number)
+ # end
+ #
+ # def after_save(record)
+ # record.credit_card_number = decrypt(record.credit_card_number)
+ # end
+ #
+ # alias_method :after_find, :after_save
+ #
+ # private
+ # def encrypt(value)
+ # # Secrecy is committed
+ # end
+ #
+ # def decrypt(value)
+ # # Secrecy is unveiled
+ # end
+ # end
+ #
+ # So you specify the object you want messaged on a given callback. When that callback is triggered, the object has
+ # a method by the name of the callback messaged. You can make these callbacks more flexible by passing in other
+ # initialization data such as the name of the attribute to work with:
+ #
+ # class BankAccount < ActiveRecord::Base
# before_save EncryptionWrapper.new("credit_card_number")
# after_save EncryptionWrapper.new("credit_card_number")
# after_initialize EncryptionWrapper.new("credit_card_number")
@@ -115,11 +146,11 @@ module ActiveRecord
# end
#
# def before_save(record)
- # record.credit_card_number = encrypt(record.credit_card_number)
+ # record.send("#{@attribute}=", encrypt(record.send("#{@attribute}")))
# end
#
# def after_save(record)
- # record.credit_card_number = decrypt(record.credit_card_number)
+ # record.send("#{@attribute}=", decrypt(record.send("#{@attribute}")))
# end
#
# alias_method :after_find, :after_save
@@ -134,9 +165,6 @@ module ActiveRecord
# end
# end
#
- # So you specify the object you want messaged on a given callback. When that callback is triggered, the object has
- # a method by the name of the callback messaged.
- #
# The callback macros usually accept a symbol for the method they're supposed to run, but you can also pass a "method string",
# which will then be evaluated within the binding of the callback. Example:
#
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 901b17124c..aac84cc5f4 100644
--- a/activerecord/lib/active_record/connection_adapters/abstract/connection_pool.rb
+++ b/activerecord/lib/active_record/connection_adapters/abstract/connection_pool.rb
@@ -351,5 +351,21 @@ module ActiveRecord
retrieve_connection_pool klass.superclass
end
end
+
+ class ConnectionManagement
+ def initialize(app)
+ @app = app
+ end
+
+ def call(env)
+ @app.call(env)
+ ensure
+ # Don't return connection (and peform implicit rollback) if
+ # this request is a part of integration test
+ unless env.key?("rack.test")
+ ActiveRecord::Base.clear_active_connections!
+ end
+ end
+ end
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 273f823e7f..24c734cddb 100644
--- a/activerecord/lib/active_record/connection_adapters/abstract/schema_definitions.rb
+++ b/activerecord/lib/active_record/connection_adapters/abstract/schema_definitions.rb
@@ -8,6 +8,7 @@ module ActiveRecord
# 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/
diff --git a/activerecord/lib/active_record/connection_adapters/postgresql_adapter.rb b/activerecord/lib/active_record/connection_adapters/postgresql_adapter.rb
index 913bb521ca..ec204d0f03 100644
--- a/activerecord/lib/active_record/connection_adapters/postgresql_adapter.rb
+++ b/activerecord/lib/active_record/connection_adapters/postgresql_adapter.rb
@@ -392,9 +392,28 @@ module ActiveRecord
quote_string(s)
end
+ # Checks the following cases:
+ #
+ # - table_name
+ # - "table.name"
+ # - schema_name.table_name
+ # - schema_name."table.name"
+ # - "schema.name".table_name
+ # - "schema.name"."table.name"
+ def quote_table_name(name)
+ schema, name_part = extract_pg_identifier_from_name(name.to_s)
+
+ unless name_part
+ quote_column_name(schema)
+ else
+ table_name, name_part = extract_pg_identifier_from_name(name_part)
+ "#{quote_column_name(schema)}.#{quote_column_name(table_name)}"
+ end
+ end
+
# Quotes column names for use in SQL queries.
def quote_column_name(name) #:nodoc:
- %("#{name}")
+ PGconn.quote_ident(name.to_s)
end
# Quote date/time values for use in SQL input. Includes microseconds
@@ -1045,6 +1064,16 @@ module ActiveRecord
ORDER BY a.attnum
end_sql
end
+
+ def extract_pg_identifier_from_name(name)
+ match_data = name[0,1] == '"' ? name.match(/\"([^\"]+)\"/) : name.match(/([^\.]+)/)
+
+ if match_data
+ rest = name[match_data[0].length..-1]
+ rest = rest[1..-1] if rest[0,1] == "."
+ [match_data[1], (rest.length > 0 ? rest : nil)]
+ end
+ end
end
end
end
diff --git a/activerecord/lib/active_record/connection_adapters/sqlite3_adapter.rb b/activerecord/lib/active_record/connection_adapters/sqlite3_adapter.rb
index cc9c46505f..75420f69aa 100644
--- a/activerecord/lib/active_record/connection_adapters/sqlite3_adapter.rb
+++ b/activerecord/lib/active_record/connection_adapters/sqlite3_adapter.rb
@@ -18,7 +18,7 @@ module ActiveRecord
db.busy_timeout(config[:timeout]) unless config[:timeout].nil?
- ConnectionAdapters::SQLite3Adapter.new(db, logger)
+ ConnectionAdapters::SQLite3Adapter.new(db, logger, config)
end
end
diff --git a/activerecord/lib/active_record/connection_adapters/sqlite_adapter.rb b/activerecord/lib/active_record/connection_adapters/sqlite_adapter.rb
index 5390f49f04..afd6472db8 100644
--- a/activerecord/lib/active_record/connection_adapters/sqlite_adapter.rb
+++ b/activerecord/lib/active_record/connection_adapters/sqlite_adapter.rb
@@ -17,9 +17,9 @@ module ActiveRecord
# "Downgrade" deprecated sqlite API
if SQLite.const_defined?(:Version)
- ConnectionAdapters::SQLite2Adapter.new(db, logger)
+ ConnectionAdapters::SQLite2Adapter.new(db, logger, config)
else
- ConnectionAdapters::DeprecatedSQLiteAdapter.new(db, logger)
+ ConnectionAdapters::DeprecatedSQLiteAdapter.new(db, logger, config)
end
end
end
@@ -72,10 +72,31 @@ module ActiveRecord
#
# * <tt>:database</tt> - Path to the database file.
class SQLiteAdapter < AbstractAdapter
+ class Version
+ include Comparable
+
+ def initialize(version_string)
+ @version = version_string.split('.').map(&:to_i)
+ end
+
+ def <=>(version_string)
+ @version <=> version_string.split('.').map(&:to_i)
+ end
+ end
+
+ def initialize(connection, logger, config)
+ super(connection, logger)
+ @config = config
+ end
+
def adapter_name #:nodoc:
'SQLite'
end
+ def supports_ddl_transactions?
+ sqlite_version >= '2.0.0'
+ end
+
def supports_migrations? #:nodoc:
true
end
@@ -83,6 +104,10 @@ module ActiveRecord
def requires_reloading?
true
end
+
+ def supports_add_column?
+ sqlite_version >= '3.1.6'
+ end
def disconnect!
super
@@ -164,7 +189,6 @@ module ActiveRecord
catch_schema_changes { @connection.rollback }
end
-
# SELECT ... FOR UPDATE is redundant since the table is locked.
def add_lock!(sql, options) #:nodoc:
sql
@@ -213,14 +237,20 @@ module ActiveRecord
execute "ALTER TABLE #{name} RENAME TO #{new_name}"
end
+ # See: http://www.sqlite.org/lang_altertable.html
+ # SQLite has an additional restriction on the ALTER TABLE statement
+ def valid_alter_table_options( type, options)
+ type.to_sym != :primary_key
+ end
+
def add_column(table_name, column_name, type, options = {}) #:nodoc:
- if @connection.respond_to?(:transaction_active?) && @connection.transaction_active?
- raise StatementInvalid, 'Cannot add columns to a SQLite database while inside a transaction'
+ if supports_add_column? && valid_alter_table_options( type, options )
+ super(table_name, column_name, type, options)
+ else
+ alter_table(table_name) do |definition|
+ definition.column(column_name, type, options)
+ end
end
-
- super(table_name, column_name, type, options)
- # See last paragraph on http://www.sqlite.org/lang_altertable.html
- execute "VACUUM"
end
def remove_column(table_name, *column_names) #:nodoc:
@@ -380,7 +410,7 @@ module ActiveRecord
end
def sqlite_version
- @sqlite_version ||= select_value('select sqlite_version(*)')
+ @sqlite_version ||= SQLiteAdapter::Version.new(select_value('select sqlite_version(*)'))
end
def default_primary_key_type
@@ -393,23 +423,9 @@ module ActiveRecord
end
class SQLite2Adapter < SQLiteAdapter # :nodoc:
- def supports_count_distinct? #:nodoc:
- false
- end
-
def rename_table(name, new_name)
move_table(name, new_name)
end
-
- def add_column(table_name, column_name, type, options = {}) #:nodoc:
- if @connection.respond_to?(:transaction_active?) && @connection.transaction_active?
- raise StatementInvalid, 'Cannot add columns to a SQLite database while inside a transaction'
- end
-
- alter_table(table_name) do |definition|
- definition.column(column_name, type, options)
- end
- end
end
class DeprecatedSQLiteAdapter < SQLite2Adapter # :nodoc:
diff --git a/activerecord/lib/active_record/fixtures.rb b/activerecord/lib/active_record/fixtures.rb
index 0131d9fac5..c6501113bf 100644
--- a/activerecord/lib/active_record/fixtures.rb
+++ b/activerecord/lib/active_record/fixtures.rb
@@ -21,13 +21,17 @@ else
end
end
-# Fixtures are a way of organizing data that you want to test against; in short, sample data. They come in 3 flavors:
+# Fixtures are a way of organizing data that you want to test against; in short, sample data.
+#
+# = Fixture formats
+#
+# Fixtures come in 3 flavors:
#
# 1. YAML fixtures
# 2. CSV fixtures
# 3. Single-file fixtures
#
-# = YAML fixtures
+# == YAML fixtures
#
# This type of fixture is in YAML format and the preferred default. YAML is a file format which describes data structures
# in a non-verbose, human-readable format. It ships with Ruby 1.8.1+.
@@ -65,9 +69,9 @@ end
# parent_id: 1
# title: Child
#
-# = CSV fixtures
+# == CSV fixtures
#
-# Fixtures can also be kept in the Comma Separated Value format. Akin to YAML fixtures, CSV fixtures are stored
+# Fixtures can also be kept in the Comma Separated Value (CSV) format. Akin to YAML fixtures, CSV fixtures are stored
# in a single file, but instead end with the <tt>.csv</tt> file extension
# (Rails example: <tt><your-rails-app>/test/fixtures/web_sites.csv</tt>).
#
@@ -90,7 +94,7 @@ end
# Most databases and spreadsheets support exporting to CSV format, so this is a great format for you to choose if you
# have existing data somewhere already.
#
-# = Single-file fixtures
+# == Single-file fixtures
#
# This type of fixture was the original format for Active Record that has since been deprecated in favor of the YAML and CSV formats.
# Fixtures for this format are created by placing text files in a sub-directory (with the name of the model) to the directory
@@ -113,65 +117,53 @@ end
# name => Ruby on Rails
# url => http://www.rubyonrails.org
#
-# = Using Fixtures
+# = Using fixtures in testcases
#
# Since fixtures are a testing construct, we use them in our unit and functional tests. There are two ways to use the
# fixtures, but first let's take a look at a sample unit test:
#
-# require 'web_site'
+# require 'test_helper'
#
# class WebSiteTest < ActiveSupport::TestCase
-# def test_web_site_count
+# test "web_site_count" do
# assert_equal 2, WebSite.count
# end
# end
#
-# As it stands, unless we pre-load the web_site table in our database with two records, this test will fail. Here's the
-# easiest way to add fixtures to the database:
-#
-# ...
-# class WebSiteTest < ActiveSupport::TestCase
-# fixtures :web_sites # add more by separating the symbols with commas
-# ...
-#
-# By adding a "fixtures" method to the test case and passing it a list of symbols (only one is shown here though), we trigger
-# the testing environment to automatically load the appropriate fixtures into the database before each test.
+# By default, the <tt>test_helper module</tt> will load all of your fixtures into your test database, so this test will succeed.
+# The testing environment will automatically load the all fixtures into the database before each test.
# To ensure consistent data, the environment deletes the fixtures before running the load.
#
-# In addition to being available in the database, the fixtures are also loaded into a hash stored in an instance variable
-# of the test case. It is named after the symbol... so, in our example, there would be a hash available called
-# <tt>@web_sites</tt>. This is where the "fixture name" comes into play.
-#
-# On top of that, each record is automatically "found" (using <tt>Model.find(id)</tt>) and placed in the instance variable of its name.
-# So for the YAML fixtures, we'd get <tt>@rubyonrails</tt> and <tt>@google</tt>, which could be interrogated using regular Active Record semantics:
+# In addition to being available in the database, the fixture's data may also be accessed by
+# using a special dynamic method, which has the same name as the model, and accepts the
+# name of the fixture to instantiate:
#
-# # test if the object created from the fixture data has the same attributes as the data itself
-# def test_find
-# assert_equal @web_sites["rubyonrails"]["name"], @rubyonrails.name
+# test "find" do
+# assert_equal "Ruby on Rails", web_sites(:rubyonrails).name
# end
#
-# As seen above, the data hash created from the YAML fixtures would have <tt>@web_sites["rubyonrails"]["url"]</tt> return
-# "http://www.rubyonrails.org" and <tt>@web_sites["google"]["name"]</tt> would return "Google". The same fixtures, but loaded
-# from a CSV fixture file, would be accessible via <tt>@web_sites["web_site_1"]["name"] == "Ruby on Rails"</tt> and have the individual
-# fixtures available as instance variables <tt>@web_site_1</tt> and <tt>@web_site_2</tt>.
+# Alternatively, you may enable auto-instantiation of the fixture data. For instance, take the following tests:
#
-# If you do not wish to use instantiated fixtures (usually for performance reasons) there are two options.
+# test "find_alt_method_1" do
+# assert_equal "Ruby on Rails", @web_sites['rubyonrails']['name']
+# end
#
-# - to completely disable instantiated fixtures:
-# self.use_instantiated_fixtures = false
+# test "find_alt_method_2" do
+# assert_equal "Ruby on Rails", @rubyonrails.news
+# end
#
-# - to keep the fixture instance (@web_sites) available, but do not automatically 'find' each instance:
-# self.use_instantiated_fixtures = :no_instances
+# In order to use these methods to access fixtured data within your testcases, you must specify one of the
+# following in your <tt>ActiveSupport::TestCase</tt>-derived class:
#
-# Even if auto-instantiated fixtures are disabled, you can still access them
-# by name via special dynamic methods. Each method has the same name as the
-# model, and accepts the name of the fixture to instantiate:
+# - to fully enable instantiated fixtures (enable alternate methods #1 and #2 above)
+# self.use_instantiated_fixtures = true
#
-# fixtures :web_sites
+# - create only the hash for the fixtures, do not 'find' each instance (enable alternate method #1 only)
+# self.use_instantiated_fixtures = :no_instances
#
-# def test_find
-# assert_equal "Ruby on Rails", web_sites(:rubyonrails).name
-# end
+# Using either of these alternate methods incurs a performance hit, as the fixtured data must be fully
+# traversed in the database to create the fixture hash and/or instance variables. This is expensive for
+# large sets of fixtured data.
#
# = Dynamic fixtures with ERb
#
@@ -194,21 +186,17 @@ end
# = Transactional fixtures
#
# TestCases can use begin+rollback to isolate their changes to the database instead of having to delete+insert for every test case.
-# They can also turn off auto-instantiation of fixture data since the feature is costly and often unused.
#
# class FooTest < ActiveSupport::TestCase
# self.use_transactional_fixtures = true
-# self.use_instantiated_fixtures = false
-#
-# fixtures :foos
#
-# def test_godzilla
+# test "godzilla" do
# assert !Foo.find(:all).empty?
# Foo.destroy_all
# assert Foo.find(:all).empty?
# end
#
-# def test_godzilla_aftermath
+# test "godzilla aftermath" do
# assert !Foo.find(:all).empty?
# end
# end
@@ -220,24 +208,25 @@ end
# access to fixture data for every table that has been loaded through fixtures (depending on the value of +use_instantiated_fixtures+)
#
# When *not* to use transactional fixtures:
-# 1. You're testing whether a transaction works correctly. Nested transactions don't commit until all parent transactions commit,
-# particularly, the fixtures transaction which is begun in setup and rolled back in teardown. Thus, you won't be able to verify
-# the results of your transaction until Active Record supports nested transactions or savepoints (in progress).
-# 2. Your database does not support transactions. Every Active Record database supports transactions except MySQL MyISAM.
-# Use InnoDB, MaxDB, or NDB instead.
+#
+# 1. You're testing whether a transaction works correctly. Nested transactions don't commit until all parent transactions commit,
+# particularly, the fixtures transaction which is begun in setup and rolled back in teardown. Thus, you won't be able to verify
+# the results of your transaction until Active Record supports nested transactions or savepoints (in progress).
+# 2. Your database does not support transactions. Every Active Record database supports transactions except MySQL MyISAM.
+# Use InnoDB, MaxDB, or NDB instead.
#
# = Advanced YAML Fixtures
#
# YAML fixtures that don't specify an ID get some extra features:
#
-# * Stable, autogenerated ID's
+# * Stable, autogenerated IDs
# * Label references for associations (belongs_to, has_one, has_many)
# * HABTM associations as inline lists
# * Autofilled timestamp columns
# * Fixture label interpolation
# * Support for YAML defaults
#
-# == Stable, autogenerated ID's
+# == Stable, autogenerated IDs
#
# Here, have a monkey fixture:
#
@@ -292,7 +281,7 @@ end
#
# Add a few more monkeys and pirates and break this into multiple files,
# and it gets pretty hard to keep track of what's going on. Let's
-# use labels instead of ID's:
+# use labels instead of IDs:
#
# ### in pirates.yml
#
diff --git a/activerecord/lib/active_record/locking/optimistic.rb b/activerecord/lib/active_record/locking/optimistic.rb
index ff9899d032..7fa7e267d8 100644
--- a/activerecord/lib/active_record/locking/optimistic.rb
+++ b/activerecord/lib/active_record/locking/optimistic.rb
@@ -23,6 +23,16 @@ module ActiveRecord
# p2.first_name = "should fail"
# p2.save # Raises a ActiveRecord::StaleObjectError
#
+ # Optimistic locking will also check for stale data when objects are destroyed. Example:
+ #
+ # p1 = Person.find(1)
+ # p2 = Person.find(1)
+ #
+ # p1.first_name = "Michael"
+ # p1.save
+ #
+ # p2.destroy # Raises a ActiveRecord::StaleObjectError
+ #
# You're then responsible for dealing with the conflict by rescuing the exception and either rolling back, merging,
# or otherwise apply the business logic needed to resolve the conflict.
#
@@ -39,6 +49,7 @@ module ActiveRecord
base.lock_optimistically = true
base.alias_method_chain :update, :lock
+ base.alias_method_chain :destroy, :lock
base.alias_method_chain :attributes_from_column_definition, :lock
class << base
@@ -98,6 +109,28 @@ module ActiveRecord
end
end
+ def destroy_with_lock #:nodoc:
+ return destroy_without_lock unless locking_enabled?
+
+ unless new_record?
+ lock_col = self.class.locking_column
+ previous_value = send(lock_col).to_i
+
+ affected_rows = connection.delete(
+ "DELETE FROM #{self.class.quoted_table_name} " +
+ "WHERE #{connection.quote_column_name(self.class.primary_key)} = #{quoted_id} " +
+ "AND #{self.class.quoted_locking_column} = #{quote_value(previous_value)}",
+ "#{self.class.name} Destroy"
+ )
+
+ unless affected_rows == 1
+ raise ActiveRecord::StaleObjectError, "Attempted to delete a stale object"
+ end
+ end
+
+ freeze
+ end
+
module ClassMethods
DEFAULT_LOCKING_COLUMN = 'lock_version'
diff --git a/activerecord/lib/active_record/migration.rb b/activerecord/lib/active_record/migration.rb
index 15350cf1e1..657acd6dc0 100644
--- a/activerecord/lib/active_record/migration.rb
+++ b/activerecord/lib/active_record/migration.rb
@@ -338,6 +338,10 @@ module ActiveRecord
self.verbose = save
end
+ def connection
+ ActiveRecord::Base.connection
+ end
+
def method_missing(method, *arguments, &block)
arg_list = arguments.map(&:inspect) * ', '
@@ -345,7 +349,7 @@ module ActiveRecord
unless arguments.empty? || method == :execute
arguments[0] = Migrator.proper_table_name(arguments.first)
end
- ActiveRecord::Base.connection.send(method, *arguments, &block)
+ connection.send(method, *arguments, &block)
end
end
end
diff --git a/activerecord/lib/active_record/named_scope.rb b/activerecord/lib/active_record/named_scope.rb
index 989b2a1ec5..1f3ef300f2 100644
--- a/activerecord/lib/active_record/named_scope.rb
+++ b/activerecord/lib/active_record/named_scope.rb
@@ -1,11 +1,12 @@
module ActiveRecord
module NamedScope
- # All subclasses of ActiveRecord::Base have two named \scopes:
- # * <tt>all</tt> - which is similar to a <tt>find(:all)</tt> query, and
+ # All subclasses of ActiveRecord::Base have one named scope:
# * <tt>scoped</tt> - which allows for the creation of anonymous \scopes, on the fly: <tt>Shirt.scoped(:conditions => {:color => 'red'}).scoped(:include => :washing_instructions)</tt>
#
# These anonymous \scopes tend to be useful when procedurally generating complex queries, where passing
# intermediate values (scopes) around as first-class objects is convenient.
+ #
+ # You can define a scope that applies to all finders using ActiveRecord::Base.default_scope.
def self.included(base)
base.class_eval do
extend ClassMethods
@@ -88,7 +89,12 @@ module ActiveRecord
when Hash
options
when Proc
- options.call(*args)
+ case parent_scope
+ when Scope
+ with_scope(:find => parent_scope.proxy_options) { options.call(*args) }
+ else
+ options.call(*args)
+ end
end, &block)
end
(class << self; self end).instance_eval do
@@ -98,9 +104,9 @@ module ActiveRecord
end
end
end
-
+
class Scope
- attr_reader :proxy_scope, :proxy_options
+ attr_reader :proxy_scope, :proxy_options, :current_scoped_methods_when_defined
NON_DELEGATE_METHODS = %w(nil? send object_id class extend find size count sum average maximum minimum paginate first last empty? any? respond_to?).to_set
[].methods.each do |m|
unless m =~ /^__/ || NON_DELEGATE_METHODS.include?(m.to_s)
@@ -111,8 +117,12 @@ module ActiveRecord
delegate :scopes, :with_scope, :to => :proxy_scope
def initialize(proxy_scope, options, &block)
+ options ||= {}
[options[:extend]].flatten.each { |extension| extend extension } if options[:extend]
extend Module.new(&block) if block_given?
+ unless Scope === proxy_scope
+ @current_scoped_methods_when_defined = proxy_scope.send(:current_scoped_methods)
+ end
@proxy_scope, @proxy_options = proxy_scope, options.except(:extend)
end
@@ -166,9 +176,15 @@ module ActiveRecord
if scopes.include?(method)
scopes[method].call(self, *args)
else
- with_scope :find => proxy_options, :create => proxy_options[:conditions].is_a?(Hash) ? proxy_options[:conditions] : {} do
+ with_scope({:find => proxy_options, :create => proxy_options[:conditions].is_a?(Hash) ? proxy_options[:conditions] : {}}, :reverse_merge) do
method = :new if method == :build
- proxy_scope.send(method, *args, &block)
+ if current_scoped_methods_when_defined
+ with_scope current_scoped_methods_when_defined do
+ proxy_scope.send(method, *args, &block)
+ end
+ else
+ proxy_scope.send(method, *args, &block)
+ end
end
end
end
diff --git a/activerecord/lib/active_record/nested_attributes.rb b/activerecord/lib/active_record/nested_attributes.rb
new file mode 100644
index 0000000000..e3122d195a
--- /dev/null
+++ b/activerecord/lib/active_record/nested_attributes.rb
@@ -0,0 +1,329 @@
+module ActiveRecord
+ module NestedAttributes #:nodoc:
+ def self.included(base)
+ base.extend(ClassMethods)
+ base.class_inheritable_accessor :reject_new_nested_attributes_procs, :instance_writer => false
+ base.reject_new_nested_attributes_procs = {}
+ end
+
+ # == Nested Attributes
+ #
+ # Nested attributes allow you to save attributes on associated records
+ # through the parent. By default nested attribute updating is turned off,
+ # you can enable it using the accepts_nested_attributes_for class method.
+ # When you enable nested attributes an attribute writer is defined on
+ # the model.
+ #
+ # The attribute writer is named after the association, which means that
+ # in the following example, two new methods are added to your model:
+ # <tt>author_attributes=(attributes)</tt> and
+ # <tt>pages_attributes=(attributes)</tt>.
+ #
+ # class Book < ActiveRecord::Base
+ # has_one :author
+ # has_many :pages
+ #
+ # accepts_nested_attributes_for :author, :pages
+ # end
+ #
+ # Note that the <tt>:autosave</tt> option is automatically enabled on every
+ # association that accepts_nested_attributes_for is used for.
+ #
+ # === One-to-one
+ #
+ # Consider a Member model that has one Avatar:
+ #
+ # class Member < ActiveRecord::Base
+ # has_one :avatar
+ # accepts_nested_attributes_for :avatar
+ # end
+ #
+ # Enabling nested attributes on a one-to-one association allows you to
+ # create the member and avatar in one go:
+ #
+ # params = { :member => { :name => 'Jack', :avatar_attributes => { :icon => 'smiling' } } }
+ # member = Member.create(params)
+ # member.avatar.id # => 2
+ # member.avatar.icon # => 'smiling'
+ #
+ # It also allows you to update the avatar through the member:
+ #
+ # params = { :member' => { :avatar_attributes => { :id => '2', :icon => 'sad' } } }
+ # member.update_attributes params['member']
+ # member.avatar.icon # => 'sad'
+ #
+ # By default you will only be able to set and update attributes on the
+ # associated model. If you want to destroy the associated model through the
+ # attributes hash, you have to enable it first using the
+ # <tt>:allow_destroy</tt> option.
+ #
+ # class Member < ActiveRecord::Base
+ # has_one :avatar
+ # accepts_nested_attributes_for :avatar, :allow_destroy => true
+ # end
+ #
+ # Now, when you add the <tt>_delete</tt> key to the attributes hash, with a
+ # value that evaluates to +true+, you will destroy the associated model:
+ #
+ # member.avatar_attributes = { :id => '2', :_delete => '1' }
+ # member.avatar.marked_for_destruction? # => true
+ # member.save
+ # member.avatar #=> nil
+ #
+ # Note that the model will _not_ be destroyed until the parent is saved.
+ #
+ # === One-to-many
+ #
+ # Consider a member that has a number of posts:
+ #
+ # class Member < ActiveRecord::Base
+ # has_many :posts
+ # accepts_nested_attributes_for :posts
+ # end
+ #
+ # You can now set or update attributes on an associated post model through
+ # the attribute hash.
+ #
+ # For each hash that does _not_ have an <tt>id</tt> key a new record will
+ # be instantiated, unless the hash also contains a <tt>_delete</tt> key
+ # that evaluates to +true+.
+ #
+ # params = { :member => {
+ # :name => 'joe', :posts_attributes => [
+ # { :title => 'Kari, the awesome Ruby documentation browser!' },
+ # { :title => 'The egalitarian assumption of the modern citizen' },
+ # { :title => '', :_delete => '1' } # this will be ignored
+ # ]
+ # }}
+ #
+ # member = Member.create(params['member'])
+ # member.posts.length # => 2
+ # member.posts.first.title # => 'Kari, the awesome Ruby documentation browser!'
+ # member.posts.second.title # => 'The egalitarian assumption of the modern citizen'
+ #
+ # You may also set a :reject_if proc to silently ignore any new record
+ # hashes if they fail to pass your criteria. For example, the previous
+ # example could be rewritten as:
+ #
+ # class Member < ActiveRecord::Base
+ # has_many :posts
+ # accepts_nested_attributes_for :posts, :reject_if => proc { |attributes| attributes['title'].blank? }
+ # end
+ #
+ # params = { :member => {
+ # :name => 'joe', :posts_attributes => [
+ # { :title => 'Kari, the awesome Ruby documentation browser!' },
+ # { :title => 'The egalitarian assumption of the modern citizen' },
+ # { :title => '' } # this will be ignored because of the :reject_if proc
+ # ]
+ # }}
+ #
+ # member = Member.create(params['member'])
+ # member.posts.length # => 2
+ # member.posts.first.title # => 'Kari, the awesome Ruby documentation browser!'
+ # member.posts.second.title # => 'The egalitarian assumption of the modern citizen'
+ #
+ # If the hash contains an <tt>id</tt> key that matches an already
+ # associated record, the matching record will be modified:
+ #
+ # member.attributes = {
+ # :name => 'Joe',
+ # :posts_attributes => [
+ # { :id => 1, :title => '[UPDATED] An, as of yet, undisclosed awesome Ruby documentation browser!' },
+ # { :id => 2, :title => '[UPDATED] other post' }
+ # ]
+ # }
+ #
+ # member.posts.first.title # => '[UPDATED] An, as of yet, undisclosed awesome Ruby documentation browser!'
+ # member.posts.second.title # => '[UPDATED] other post'
+ #
+ # By default the associated records are protected from being destroyed. If
+ # you want to destroy any of the associated records through the attributes
+ # hash, you have to enable it first using the <tt>:allow_destroy</tt>
+ # option. This will allow you to also use the <tt>_delete</tt> key to
+ # destroy existing records:
+ #
+ # class Member < ActiveRecord::Base
+ # has_many :posts
+ # accepts_nested_attributes_for :posts, :allow_destroy => true
+ # end
+ #
+ # params = { :member => {
+ # :posts_attributes => [{ :id => '2', :_delete => '1' }]
+ # }}
+ #
+ # member.attributes = params['member']
+ # member.posts.detect { |p| p.id == 2 }.marked_for_destruction? # => true
+ # member.posts.length #=> 2
+ # member.save
+ # member.posts.length # => 1
+ #
+ # === Saving
+ #
+ # All changes to models, including the destruction of those marked for
+ # destruction, are saved and destroyed automatically and atomically when
+ # the parent model is saved. This happens inside the transaction initiated
+ # by the parents save method. See ActiveRecord::AutosaveAssociation.
+ module ClassMethods
+ # Defines an attributes writer for the specified association(s). If you
+ # are using <tt>attr_protected</tt> or <tt>attr_accessible</tt>, then you
+ # will need to add the attribute writer to the allowed list.
+ #
+ # Supported options:
+ # [:allow_destroy]
+ # If true, destroys any members from the attributes hash with a
+ # <tt>_delete</tt> key and a value that evaluates to +true+
+ # (eg. 1, '1', true, or 'true'). This option is off by default.
+ # [:reject_if]
+ # Allows you to specify a Proc that checks whether a record should be
+ # built for a certain attribute hash. The hash is passed to the Proc
+ # and the Proc should return either +true+ or +false+. When no Proc
+ # is specified a record will be built for all attribute hashes that
+ # do not have a <tt>_delete</tt> that evaluates to true.
+ #
+ # Examples:
+ # # creates avatar_attributes=
+ # accepts_nested_attributes_for :avatar, :reject_if => proc { |attributes| attributes['name'].blank? }
+ # # creates avatar_attributes= and posts_attributes=
+ # accepts_nested_attributes_for :avatar, :posts, :allow_destroy => true
+ def accepts_nested_attributes_for(*attr_names)
+ options = { :allow_destroy => false }
+ options.update(attr_names.extract_options!)
+ options.assert_valid_keys(:allow_destroy, :reject_if)
+
+ attr_names.each do |association_name|
+ if reflection = reflect_on_association(association_name)
+ type = case reflection.macro
+ when :has_one, :belongs_to
+ :one_to_one
+ when :has_many, :has_and_belongs_to_many
+ :collection
+ end
+
+ reflection.options[:autosave] = true
+ self.reject_new_nested_attributes_procs[association_name.to_sym] = options[:reject_if]
+
+ # def pirate_attributes=(attributes)
+ # assign_nested_attributes_for_one_to_one_association(:pirate, attributes, false)
+ # end
+ class_eval %{
+ def #{association_name}_attributes=(attributes)
+ assign_nested_attributes_for_#{type}_association(:#{association_name}, attributes, #{options[:allow_destroy]})
+ end
+ }, __FILE__, __LINE__
+ else
+ raise ArgumentError, "No association found for name `#{association_name}'. Has it been defined yet?"
+ end
+ end
+ end
+ end
+
+ # Returns ActiveRecord::AutosaveAssociation::marked_for_destruction? It's
+ # used in conjunction with fields_for to build a form element for the
+ # destruction of this association.
+ #
+ # See ActionView::Helpers::FormHelper::fields_for for more info.
+ def _delete
+ marked_for_destruction?
+ end
+
+ private
+
+ # Attribute hash keys that should not be assigned as normal attributes.
+ # These hash keys are nested attributes implementation details.
+ UNASSIGNABLE_KEYS = %w{ id _delete }
+
+ # Assigns the given attributes to the association.
+ #
+ # If the given attributes include an <tt>:id</tt> that matches the existing
+ # record’s id, then the existing record will be modified. Otherwise a new
+ # record will be built.
+ #
+ # If the given attributes include a matching <tt>:id</tt> attribute _and_ a
+ # <tt>:_delete</tt> key set to a truthy value, then the existing record
+ # will be marked for destruction.
+ def assign_nested_attributes_for_one_to_one_association(association_name, attributes, allow_destroy)
+ attributes = attributes.stringify_keys
+
+ if attributes['id'].blank?
+ unless reject_new_record?(association_name, attributes)
+ send("build_#{association_name}", attributes.except(*UNASSIGNABLE_KEYS))
+ end
+ elsif (existing_record = send(association_name)) && existing_record.id.to_s == attributes['id'].to_s
+ assign_to_or_mark_for_destruction(existing_record, attributes, allow_destroy)
+ end
+ end
+
+ # Assigns the given attributes to the collection association.
+ #
+ # Hashes with an <tt>:id</tt> value matching an existing associated record
+ # will update that record. Hashes without an <tt>:id</tt> value will build
+ # a new record for the association. Hashes with a matching <tt>:id</tt>
+ # value and a <tt>:_delete</tt> key set to a truthy value will mark the
+ # matched record for destruction.
+ #
+ # For example:
+ #
+ # assign_nested_attributes_for_collection_association(:people, {
+ # '1' => { :id => '1', :name => 'Peter' },
+ # '2' => { :name => 'John' },
+ # '3' => { :id => '2', :_delete => true }
+ # })
+ #
+ # Will update the name of the Person with ID 1, build a new associated
+ # person with the name `John', and mark the associatied Person with ID 2
+ # for destruction.
+ #
+ # Also accepts an Array of attribute hashes:
+ #
+ # assign_nested_attributes_for_collection_association(:people, [
+ # { :id => '1', :name => 'Peter' },
+ # { :name => 'John' },
+ # { :id => '2', :_delete => true }
+ # ])
+ def assign_nested_attributes_for_collection_association(association_name, attributes_collection, allow_destroy)
+ unless attributes_collection.is_a?(Hash) || attributes_collection.is_a?(Array)
+ raise ArgumentError, "Hash or Array expected, got #{attributes_collection.class.name} (#{attributes_collection.inspect})"
+ end
+
+ if attributes_collection.is_a? Hash
+ attributes_collection = attributes_collection.sort_by { |index, _| index.to_i }.map { |_, attributes| attributes }
+ end
+
+ attributes_collection.each do |attributes|
+ attributes = attributes.stringify_keys
+
+ if attributes['id'].blank?
+ unless reject_new_record?(association_name, attributes)
+ send(association_name).build(attributes.except(*UNASSIGNABLE_KEYS))
+ end
+ elsif existing_record = send(association_name).detect { |record| record.id.to_s == attributes['id'].to_s }
+ assign_to_or_mark_for_destruction(existing_record, attributes, allow_destroy)
+ end
+ end
+ end
+
+ # Updates a record with the +attributes+ or marks it for destruction if
+ # +allow_destroy+ is +true+ and has_delete_flag? returns +true+.
+ def assign_to_or_mark_for_destruction(record, attributes, allow_destroy)
+ if has_delete_flag?(attributes) && allow_destroy
+ record.mark_for_destruction
+ else
+ record.attributes = attributes.except(*UNASSIGNABLE_KEYS)
+ end
+ end
+
+ # Determines if a hash contains a truthy _delete key.
+ def has_delete_flag?(hash)
+ ConnectionAdapters::Column.value_to_boolean hash['_delete']
+ end
+
+ # Determines if a new record should be build by checking for
+ # has_delete_flag? or if a <tt>:reject_if</tt> proc exists for this
+ # association and evaluates to +true+.
+ def reject_new_record?(association_name, attributes)
+ has_delete_flag?(attributes) ||
+ self.class.reject_new_nested_attributes_procs[association_name].try(:call, attributes)
+ end
+ end
+end
diff --git a/activerecord/lib/active_record/reflection.rb b/activerecord/lib/active_record/reflection.rb
index 1937abdc83..2d4c1d5507 100644
--- a/activerecord/lib/active_record/reflection.rb
+++ b/activerecord/lib/active_record/reflection.rb
@@ -65,6 +65,11 @@ module ActiveRecord
def reflect_on_association(association)
reflections[association].is_a?(AssociationReflection) ? reflections[association] : nil
end
+
+ # Returns an array of AssociationReflection objects for all associations which have <tt>:autosave</tt> enabled.
+ def reflect_on_all_autosave_associations
+ reflections.values.select { |reflection| reflection.options[:autosave] }
+ end
end
@@ -192,7 +197,7 @@ module ActiveRecord
def counter_cache_column
if options[:counter_cache] == true
- "#{active_record.name.underscore.pluralize}_count"
+ "#{active_record.name.demodulize.underscore.pluralize}_count"
elsif options[:counter_cache]
options[:counter_cache]
end
diff --git a/activerecord/lib/active_record/serializers/json_serializer.rb b/activerecord/lib/active_record/serializers/json_serializer.rb
index 419b45d475..1fd65ed006 100644
--- a/activerecord/lib/active_record/serializers/json_serializer.rb
+++ b/activerecord/lib/active_record/serializers/json_serializer.rb
@@ -8,6 +8,25 @@ module ActiveRecord #:nodoc:
# Returns a JSON string representing the model. Some configuration is
# available through +options+.
#
+ # The option <tt>ActiveRecord::Base.include_root_in_json</tt> controls the
+ # top-level behavior of to_json. In a new Rails application, it is set to
+ # <tt>true</tt> in initializers/new_rails_defaults.rb. When it is <tt>true</tt>,
+ # to_json will emit a single root node named after the object's type. For example:
+ #
+ # konata = User.find(1)
+ # ActiveRecord::Base.include_root_in_json = true
+ # konata.to_json
+ # # => { "user": {"id": 1, "name": "Konata Izumi", "age": 16,
+ # "created_at": "2006/08/01", "awesome": true} }
+ #
+ # ActiveRecord::Base.include_root_in_json = false
+ # konata.to_json
+ # # => {"id": 1, "name": "Konata Izumi", "age": 16,
+ # "created_at": "2006/08/01", "awesome": true}
+ #
+ # The remainder of the examples in this section assume include_root_in_json is set to
+ # <tt>false</tt>.
+ #
# Without any +options+, the returned JSON string will include all
# the model's attributes. For example:
#
diff --git a/activerecord/lib/active_record/serializers/xml_serializer.rb b/activerecord/lib/active_record/serializers/xml_serializer.rb
index 4749823b94..fa75874603 100644
--- a/activerecord/lib/active_record/serializers/xml_serializer.rb
+++ b/activerecord/lib/active_record/serializers/xml_serializer.rb
@@ -231,16 +231,22 @@ module ActiveRecord #:nodoc:
def add_associations(association, records, opts)
if records.is_a?(Enumerable)
tag = reformat_name(association.to_s)
+ type = options[:skip_types] ? {} : {:type => "array"}
+
if records.empty?
- builder.tag!(tag, :type => :array)
+ builder.tag!(tag, type)
else
- builder.tag!(tag, :type => :array) do
+ builder.tag!(tag, type) do
association_name = association.to_s.singularize
records.each do |record|
- record.to_xml opts.merge(
- :root => association_name,
- :type => (record.class.to_s.underscore == association_name ? nil : record.class.name)
- )
+ if options[:skip_types]
+ record_type = {}
+ else
+ record_class = (record.class.to_s.underscore == association_name) ? nil : record.class.name
+ record_type = {:type => record_class}
+ end
+
+ record.to_xml opts.merge(:root => association_name).merge(record_type)
end
end
end
diff --git a/activerecord/lib/active_record/session_store.rb b/activerecord/lib/active_record/session_store.rb
index 74d91f129e..21471da419 100644
--- a/activerecord/lib/active_record/session_store.rb
+++ b/activerecord/lib/active_record/session_store.rb
@@ -99,7 +99,7 @@ module ActiveRecord
define_method(:session_id=) { |session_id| self.sessid = session_id }
else
def self.find_by_session_id(session_id)
- find :first, :conditions => ["session_id #{attribute_condition(session_id)}", session_id]
+ find :first, :conditions => {:session_id=>session_id}
end
end
end
@@ -184,7 +184,7 @@ module ActiveRecord
# Look up a session by id and unmarshal its data if found.
def find_by_session_id(session_id)
- if record = @@connection.select_one("SELECT * FROM #{@@table_name} WHERE #{@@session_id_column}=#{@@connection.quote(session_id)}")
+ if record = connection.select_one("SELECT * FROM #{@@table_name} WHERE #{@@session_id_column}=#{connection.quote(session_id)}")
new(:session_id => session_id, :marshaled_data => record['data'])
end
end
@@ -287,8 +287,7 @@ module ActiveRecord
def get_session(env, sid)
Base.silence do
sid ||= generate_sid
- session = @@session_class.find_by_session_id(sid)
- session ||= @@session_class.new(:session_id => sid, :data => {})
+ session = find_session(sid)
env[SESSION_RECORD_KEY] = session
[sid, session.data]
end
@@ -296,7 +295,7 @@ module ActiveRecord
def set_session(env, sid, session_data)
Base.silence do
- record = env[SESSION_RECORD_KEY]
+ record = env[SESSION_RECORD_KEY] ||= find_session(sid)
record.data = session_data
return false unless record.save
@@ -310,5 +309,10 @@ module ActiveRecord
return true
end
+
+ def find_session(id)
+ @@session_class.find_by_session_id(id) ||
+ @@session_class.new(:session_id => id, :data => {})
+ end
end
end
diff --git a/activerecord/lib/active_record/test_case.rb b/activerecord/lib/active_record/test_case.rb
index 149b93203e..8c6abaaccb 100644
--- a/activerecord/lib/active_record/test_case.rb
+++ b/activerecord/lib/active_record/test_case.rb
@@ -27,6 +27,7 @@ module ActiveRecord
$queries_executed = []
yield
ensure
+ %w{ BEGIN COMMIT }.each { |x| $queries_executed.delete(x) }
assert_equal num, $queries_executed.size, "#{$queries_executed.size} instead of #{num} queries were executed.#{$queries_executed.size == 0 ? '' : "\nQueries:\n#{$queries_executed.join("\n")}"}"
end
@@ -48,5 +49,18 @@ module ActiveRecord
ActiveRecord::Base.clear_all_connections!
ActiveRecord::Base.establish_connection(@connection)
end
+
+ def with_kcode(kcode)
+ if RUBY_VERSION < '1.9'
+ orig_kcode, $KCODE = $KCODE, kcode
+ begin
+ yield
+ ensure
+ $KCODE = orig_kcode
+ end
+ else
+ yield
+ end
+ end
end
end
diff --git a/activerecord/lib/active_record/transactions.rb b/activerecord/lib/active_record/transactions.rb
index 0b6e52c79b..b059eb7f6f 100644
--- a/activerecord/lib/active_record/transactions.rb
+++ b/activerecord/lib/active_record/transactions.rb
@@ -175,6 +175,8 @@ module ActiveRecord
# end # RELEASE savepoint active_record_1
# # ^^^^ BOOM! database error!
# end
+ #
+ # Note that "TRUNCATE" is also a MySQL DDL statement!
module ClassMethods
# See ActiveRecord::Transactions::ClassMethods for detailed documentation.
def transaction(options = {}, &block)
diff --git a/activerecord/lib/active_record/validations.rb b/activerecord/lib/active_record/validations.rb
index 6d750accb0..d2d12b80c9 100644
--- a/activerecord/lib/active_record/validations.rb
+++ b/activerecord/lib/active_record/validations.rb
@@ -89,7 +89,7 @@ module ActiveRecord
message, options[:default] = options[:default], message if options[:default].is_a?(Symbol)
- defaults = @base.class.self_and_descendents_from_active_record.map do |klass|
+ defaults = @base.class.self_and_descendants_from_active_record.map do |klass|
[ :"models.#{klass.name.underscore}.attributes.#{attribute}.#{message}",
:"models.#{klass.name.underscore}.#{message}" ]
end
@@ -575,6 +575,8 @@ module ActiveRecord
# Get range option and value.
option = range_options.first
option_value = options[range_options.first]
+ key = {:is => :wrong_length, :minimum => :too_short, :maximum => :too_long}[option]
+ custom_message = options[:message] || options[key]
case option
when :within, :in
@@ -583,9 +585,9 @@ module ActiveRecord
validates_each(attrs, options) do |record, attr, value|
value = options[:tokenizer].call(value) if value.kind_of?(String)
if value.nil? or value.size < option_value.begin
- record.errors.add(attr, :too_short, :default => options[:too_short], :count => option_value.begin)
+ record.errors.add(attr, :too_short, :default => custom_message || options[:too_short], :count => option_value.begin)
elsif value.size > option_value.end
- record.errors.add(attr, :too_long, :default => options[:too_long], :count => option_value.end)
+ record.errors.add(attr, :too_long, :default => custom_message || options[:too_long], :count => option_value.end)
end
end
when :is, :minimum, :maximum
@@ -593,13 +595,10 @@ module ActiveRecord
# Declare different validations per option.
validity_checks = { :is => "==", :minimum => ">=", :maximum => "<=" }
- message_options = { :is => :wrong_length, :minimum => :too_short, :maximum => :too_long }
validates_each(attrs, options) do |record, attr, value|
value = options[:tokenizer].call(value) if value.kind_of?(String)
unless !value.nil? and value.size.method(validity_checks[option])[option_value]
- key = message_options[option]
- custom_message = options[:message] || options[key]
record.errors.add(attr, key, :default => custom_message, :count => option_value)
end
end
@@ -721,20 +720,20 @@ module ActiveRecord
# class (which has a database table to query from).
finder_class = class_hierarchy.detect { |klass| !klass.abstract_class? }
- is_text_column = finder_class.columns_hash[attr_name.to_s].text?
+ column = finder_class.columns_hash[attr_name.to_s]
if value.nil?
comparison_operator = "IS ?"
- elsif is_text_column
+ elsif column.text?
comparison_operator = "#{connection.case_sensitive_equality_operator} ?"
- value = value.to_s
+ value = column.limit ? value.to_s[0, column.limit] : value.to_s
else
comparison_operator = "= ?"
end
sql_attribute = "#{record.class.quoted_table_name}.#{connection.quote_column_name(attr_name)}"
- if value.nil? || (configuration[:case_sensitive] || !is_text_column)
+ if value.nil? || (configuration[:case_sensitive] || !column.text?)
condition_sql = "#{sql_attribute} #{comparison_operator}"
condition_params = [value]
else
@@ -745,7 +744,7 @@ module ActiveRecord
if scope = configuration[:scope]
Array(scope).map do |scope_item|
scope_value = record.send(scope_item)
- condition_sql << " AND #{record.class.quoted_table_name}.#{scope_item} #{attribute_condition(scope_value)}"
+ condition_sql << " AND " << attribute_condition("#{record.class.quoted_table_name}.#{scope_item}", scope_value)
condition_params << scope_value
end
end
@@ -803,7 +802,7 @@ module ActiveRecord
# Validates whether the value of the specified attribute is available in a particular enumerable object.
#
# class Person < ActiveRecord::Base
- # validates_inclusion_of :gender, :in => %w( m f ), :message => "woah! what are you then!??!!"
+ # validates_inclusion_of :gender, :in => %w( m f )
# validates_inclusion_of :age, :in => 0..99
# validates_inclusion_of :format, :in => %w( jpg gif png ), :message => "extension {{value}} is not included in the list"
# end
@@ -1041,6 +1040,11 @@ module ActiveRecord
errors.empty?
end
+ # Performs the opposite of <tt>valid?</tt>. Returns true if errors were added, false otherwise.
+ def invalid?
+ !valid?
+ end
+
# Returns the Errors object that holds all information about attribute error messages.
def errors
@errors ||= Errors.new(self)
diff --git a/activerecord/lib/active_record/version.rb b/activerecord/lib/active_record/version.rb
index 6ac4bdc905..852807b4c5 100644
--- a/activerecord/lib/active_record/version.rb
+++ b/activerecord/lib/active_record/version.rb
@@ -2,7 +2,7 @@ module ActiveRecord
module VERSION #:nodoc:
MAJOR = 2
MINOR = 3
- TINY = 0
+ TINY = 2
STRING = [MAJOR, MINOR, TINY].join('.')
end