aboutsummaryrefslogtreecommitdiffstats
path: root/activerecord/lib
diff options
context:
space:
mode:
authorPratik Naik <pratiknaik@gmail.com>2009-03-10 00:33:32 +0000
committerPratik Naik <pratiknaik@gmail.com>2009-03-10 00:33:32 +0000
commit1ab84247ca4b80afbd82afe7539b3449636344c4 (patch)
tree9ca5e3bee3a07b68c9d900ff129b69bc740405c2 /activerecord/lib
parentb6fb79c531a0079274f7b459302e6290c9183f28 (diff)
parent4458edc882b229ea44602da20a6440a6f233f1c8 (diff)
downloadrails-1ab84247ca4b80afbd82afe7539b3449636344c4.tar.gz
rails-1ab84247ca4b80afbd82afe7539b3449636344c4.tar.bz2
rails-1ab84247ca4b80afbd82afe7539b3449636344c4.zip
Merge commit 'mainstream/master'
Conflicts: railties/guides/files/stylesheets/main.css railties/guides/rails_guides/generator.rb railties/guides/source/index.erb.textile
Diffstat (limited to 'activerecord/lib')
-rwxr-xr-xactiverecord/lib/active_record/associations.rb138
-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/autosave_association.rb240
-rwxr-xr-xactiverecord/lib/active_record/base.rb10
-rw-r--r--activerecord/lib/active_record/calculations.rb26
-rw-r--r--activerecord/lib/active_record/connection_adapters/abstract/connection_pool.rb16
-rw-r--r--activerecord/lib/active_record/connection_adapters/sqlite3_adapter.rb2
-rw-r--r--activerecord/lib/active_record/connection_adapters/sqlite_adapter.rb9
-rw-r--r--activerecord/lib/active_record/locking/optimistic.rb33
-rw-r--r--activerecord/lib/active_record/named_scope.rb25
-rw-r--r--activerecord/lib/active_record/reflection.rb2
-rw-r--r--activerecord/lib/active_record/session_store.rb10
-rw-r--r--activerecord/lib/active_record/test_case.rb13
-rw-r--r--activerecord/lib/active_record/validations.rb13
-rw-r--r--activerecord/lib/active_record/version.rb2
18 files changed, 348 insertions, 222 deletions
diff --git a/activerecord/lib/active_record/associations.rb b/activerecord/lib/active_record/associations.rb
index e2dc883b1b..301b3a3b58 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'
@@ -786,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]
@@ -872,10 +874,10 @@ 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]
@@ -898,22 +900,9 @@ module ActiveRecord
association_accessor_methods(reflection, ActiveRecord::Associations::HasOneThroughAssociation)
else
reflection = create_has_one_reflection(association_id, options)
-
- method_name = "has_one_after_save_for_#{reflection.name}".to_sym
- define_method(method_name) do
- association = association_instance_get(reflection.name)
- if association && (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
@@ -1006,47 +995,15 @@ module ActiveRecord
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 = association_instance_get(reflection.name)
- 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
- if association = association_instance_get(reflection.name)
- 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
@@ -1067,8 +1024,6 @@ module ActiveRecord
)
end
- add_single_associated_validation_callbacks(reflection.name) if options[:validate] == true
-
configure_dependency_for_belongs_to(reflection)
end
@@ -1234,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
@@ -1358,70 +1310,6 @@ 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
- if association = association_instance_get(association_name)
- 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
- define_method(method_name) do
- association = association_instance_get(association_name)
-
- if association
- 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)
- 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 = association_instance_get(association_name)
-
- records_to_save = if @new_record_before_save
- association
- elsif association && association.loaded?
- association.select { |record| record.new_record? }
- elsif association && !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|
attributees = params.first unless params.empty?
@@ -1642,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))
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/autosave_association.rb b/activerecord/lib/active_record/autosave_association.rb
index 680b41518c..1c3d0567c1 100644
--- a/activerecord/lib/active_record/autosave_association.rb
+++ b/activerecord/lib/active_record/autosave_association.rb
@@ -125,79 +125,63 @@ module ActiveRecord
# 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
- alias_method_chain :save, :autosave_associations
- alias_method_chain :save!, :autosave_associations
- alias_method_chain :valid?, :autosave_associations
- %w{ has_one belongs_to has_many has_and_belongs_to_many }.each do |type|
+ ASSOCIATION_TYPES.each do |type|
base.send("valid_keys_for_#{type}_association") << :autosave
end
end
end
- # Saves the parent, <tt>self</tt>, and any loaded autosave associations.
- # 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_with_autosave_associations(perform_validation = true)
- returning(save_without_autosave_associations(perform_validation)) do |valid|
- if valid
- self.class.reflect_on_all_autosave_associations.each do |reflection|
- if (association = association_instance_get(reflection.name)) && association.loaded?
- if association.is_a?(Array)
- association.proxy_target.each do |child|
- child.marked_for_destruction? ? child.destroy : child.save(perform_validation)
- end
- else
- association.marked_for_destruction? ? association.destroy : association.save(perform_validation)
- 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
+ }
end
- end
- # Attempts to save the record just like save_with_autosave_associations but
- # will raise a RecordInvalid exception instead of returning false if the
- # record is not valid.
- def save_with_autosave_associations!
- if valid_with_autosave_associations?
- save_with_autosave_associations(false) || raise(RecordNotSaved)
- else
- raise RecordInvalid.new(self)
- 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
- # Returns whether or not the parent, <tt>self</tt>, and any loaded autosave associations are valid.
- def valid_with_autosave_associations?
- if valid_without_autosave_associations?
- self.class.reflect_on_all_autosave_associations.all? do |reflection|
- if (association = association_instance_get(reflection.name)) && association.loaded?
- if association.is_a?(Array)
- association.proxy_target.all? { |child| autosave_association_valid?(reflection, child) }
- else
- autosave_association_valid?(reflection, association)
- end
- else
- true # association not loaded yet, so it should be valid
+ 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
- else
- false # self was not valid
- end
- end
-
- # Returns whether or not the association is valid and applies any errors to the parent, <tt>self</tt>, if it wasn't.
- def autosave_association_valid?(reflection, association)
- returning(association.valid?) do |valid|
- association.errors.each do |attribute, message|
- errors.add "#{reflection.name}_#{attribute}", message
- end unless valid
end
end
@@ -221,5 +205,145 @@ module ActiveRecord
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?
+ record.destroy
+ 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)
+ 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 cfe1f8b8ef..75f84ef2ed 100755
--- a/activerecord/lib/active_record/base.rb
+++ b/activerecord/lib/active_record/base.rb
@@ -1537,7 +1537,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)$/)
@@ -1690,7 +1690,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)
@@ -1754,12 +1754,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
@@ -2174,7 +2174,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
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/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/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..6077ddcdb6 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,6 +72,11 @@ module ActiveRecord
#
# * <tt>:database</tt> - Path to the database file.
class SQLiteAdapter < AbstractAdapter
+ def initialize(connection, logger, config)
+ super(connection, logger)
+ @config = config
+ end
+
def adapter_name #:nodoc:
'SQLite'
end
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/named_scope.rb b/activerecord/lib/active_record/named_scope.rb
index 989b2a1ec5..43411dfb55 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
@@ -100,7 +106,7 @@ module ActiveRecord
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)
@@ -113,6 +119,9 @@ module ActiveRecord
def initialize(proxy_scope, options, &block)
[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
@@ -168,7 +177,13 @@ module ActiveRecord
else
with_scope :find => proxy_options, :create => proxy_options[:conditions].is_a?(Hash) ? proxy_options[:conditions] : {} 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/reflection.rb b/activerecord/lib/active_record/reflection.rb
index e69bfb1355..2d4c1d5507 100644
--- a/activerecord/lib/active_record/reflection.rb
+++ b/activerecord/lib/active_record/reflection.rb
@@ -197,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/session_store.rb b/activerecord/lib/active_record/session_store.rb
index de199d30bf..3cc4640f42 100644
--- a/activerecord/lib/active_record/session_store.rb
+++ b/activerecord/lib/active_record/session_store.rb
@@ -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 211dd78874..8c6abaaccb 100644
--- a/activerecord/lib/active_record/test_case.rb
+++ b/activerecord/lib/active_record/test_case.rb
@@ -49,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/validations.rb b/activerecord/lib/active_record/validations.rb
index 8862bd05d6..e2b596446f 100644
--- a/activerecord/lib/active_record/validations.rb
+++ b/activerecord/lib/active_record/validations.rb
@@ -720,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
@@ -1040,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..809e91897e 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 = 1
STRING = [MAJOR, MINOR, TINY].join('.')
end