aboutsummaryrefslogtreecommitdiffstats
path: root/activerecord/lib/active_record
diff options
context:
space:
mode:
Diffstat (limited to 'activerecord/lib/active_record')
-rw-r--r--activerecord/lib/active_record/aggregations.rb163
-rw-r--r--activerecord/lib/active_record/association_preload.rb403
-rw-r--r--activerecord/lib/active_record/associations.rb1526
-rw-r--r--activerecord/lib/active_record/associations/alias_tracker.rb85
-rw-r--r--activerecord/lib/active_record/associations/association.rb230
-rw-r--r--activerecord/lib/active_record/associations/association_collection.rb557
-rw-r--r--activerecord/lib/active_record/associations/association_proxy.rb295
-rw-r--r--activerecord/lib/active_record/associations/association_scope.rb120
-rw-r--r--activerecord/lib/active_record/associations/belongs_to_association.rb102
-rw-r--r--activerecord/lib/active_record/associations/belongs_to_polymorphic_association.rb74
-rw-r--r--activerecord/lib/active_record/associations/builder/association.rb53
-rw-r--r--activerecord/lib/active_record/associations/builder/belongs_to.rb83
-rw-r--r--activerecord/lib/active_record/associations/builder/collection_association.rb75
-rw-r--r--activerecord/lib/active_record/associations/builder/has_and_belongs_to_many.rb63
-rw-r--r--activerecord/lib/active_record/associations/builder/has_many.rb63
-rw-r--r--activerecord/lib/active_record/associations/builder/has_one.rb61
-rw-r--r--activerecord/lib/active_record/associations/builder/singular_association.rb32
-rw-r--r--activerecord/lib/active_record/associations/collection_association.rb536
-rw-r--r--activerecord/lib/active_record/associations/collection_proxy.rb127
-rw-r--r--activerecord/lib/active_record/associations/has_and_belongs_to_many_association.rb143
-rw-r--r--activerecord/lib/active_record/associations/has_many_association.rb129
-rw-r--r--activerecord/lib/active_record/associations/has_many_through_association.rb166
-rw-r--r--activerecord/lib/active_record/associations/has_one_association.rb158
-rw-r--r--activerecord/lib/active_record/associations/has_one_through_association.rb46
-rw-r--r--activerecord/lib/active_record/associations/join_dependency.rb215
-rw-r--r--activerecord/lib/active_record/associations/join_dependency/join_association.rb144
-rw-r--r--activerecord/lib/active_record/associations/join_dependency/join_base.rb24
-rw-r--r--activerecord/lib/active_record/associations/join_dependency/join_part.rb78
-rw-r--r--activerecord/lib/active_record/associations/join_helper.rb56
-rw-r--r--activerecord/lib/active_record/associations/preloader.rb177
-rw-r--r--activerecord/lib/active_record/associations/preloader/association.rb126
-rw-r--r--activerecord/lib/active_record/associations/preloader/belongs_to.rb17
-rw-r--r--activerecord/lib/active_record/associations/preloader/collection_association.rb24
-rw-r--r--activerecord/lib/active_record/associations/preloader/has_and_belongs_to_many.rb60
-rw-r--r--activerecord/lib/active_record/associations/preloader/has_many.rb17
-rw-r--r--activerecord/lib/active_record/associations/preloader/has_many_through.rb15
-rw-r--r--activerecord/lib/active_record/associations/preloader/has_one.rb23
-rw-r--r--activerecord/lib/active_record/associations/preloader/has_one_through.rb9
-rw-r--r--activerecord/lib/active_record/associations/preloader/singular_association.rb21
-rw-r--r--activerecord/lib/active_record/associations/preloader/through_association.rb67
-rw-r--r--activerecord/lib/active_record/associations/singular_association.rb55
-rw-r--r--activerecord/lib/active_record/associations/through_association.rb80
-rw-r--r--activerecord/lib/active_record/associations/through_association_scope.rb154
-rw-r--r--activerecord/lib/active_record/attribute_methods.rb28
-rw-r--r--activerecord/lib/active_record/attribute_methods/before_type_cast.rb20
-rw-r--r--activerecord/lib/active_record/attribute_methods/dirty.rb10
-rw-r--r--activerecord/lib/active_record/attribute_methods/primary_key.rb41
-rw-r--r--activerecord/lib/active_record/attribute_methods/read.rb54
-rw-r--r--activerecord/lib/active_record/attribute_methods/time_zone_conversion.rb21
-rw-r--r--activerecord/lib/active_record/attribute_methods/write.rb10
-rw-r--r--activerecord/lib/active_record/autosave_association.rb218
-rw-r--r--activerecord/lib/active_record/base.rb703
-rw-r--r--activerecord/lib/active_record/callbacks.rb99
-rw-r--r--activerecord/lib/active_record/coders/yaml_column.rb41
-rw-r--r--activerecord/lib/active_record/connection_adapters/abstract/connection_pool.rb92
-rw-r--r--activerecord/lib/active_record/connection_adapters/abstract/connection_specification.rb16
-rw-r--r--activerecord/lib/active_record/connection_adapters/abstract/database_limits.rb4
-rw-r--r--activerecord/lib/active_record/connection_adapters/abstract/database_statements.rb82
-rw-r--r--activerecord/lib/active_record/connection_adapters/abstract/query_cache.rb32
-rw-r--r--activerecord/lib/active_record/connection_adapters/abstract/quoting.rb46
-rw-r--r--activerecord/lib/active_record/connection_adapters/abstract/schema_definitions.rb283
-rw-r--r--activerecord/lib/active_record/connection_adapters/abstract/schema_statements.rb128
-rw-r--r--activerecord/lib/active_record/connection_adapters/abstract_adapter.rb38
-rw-r--r--activerecord/lib/active_record/connection_adapters/column.rb268
-rw-r--r--activerecord/lib/active_record/connection_adapters/mysql2_adapter.rb101
-rw-r--r--activerecord/lib/active_record/connection_adapters/mysql_adapter.rb292
-rw-r--r--activerecord/lib/active_record/connection_adapters/postgresql_adapter.rb201
-rw-r--r--activerecord/lib/active_record/connection_adapters/sqlite3_adapter.rb18
-rw-r--r--activerecord/lib/active_record/connection_adapters/sqlite_adapter.rb117
-rw-r--r--activerecord/lib/active_record/counter_cache.rb11
-rw-r--r--activerecord/lib/active_record/dynamic_finder_match.rb39
-rw-r--r--activerecord/lib/active_record/dynamic_scope_match.rb23
-rw-r--r--activerecord/lib/active_record/errors.rb8
-rw-r--r--activerecord/lib/active_record/fixtures.rb406
-rw-r--r--activerecord/lib/active_record/identity_map.rb102
-rw-r--r--activerecord/lib/active_record/locale/en.yml4
-rw-r--r--activerecord/lib/active_record/locking/optimistic.rb34
-rw-r--r--activerecord/lib/active_record/locking/pessimistic.rb16
-rw-r--r--activerecord/lib/active_record/log_subscriber.rb16
-rw-r--r--activerecord/lib/active_record/migration.rb475
-rw-r--r--activerecord/lib/active_record/migration/command_recorder.rb103
-rw-r--r--activerecord/lib/active_record/named_scope.rb55
-rw-r--r--activerecord/lib/active_record/nested_attributes.rb106
-rw-r--r--activerecord/lib/active_record/observer.rb47
-rw-r--r--activerecord/lib/active_record/persistence.rb123
-rw-r--r--activerecord/lib/active_record/railtie.rb33
-rw-r--r--activerecord/lib/active_record/railties/databases.rake120
-rw-r--r--activerecord/lib/active_record/reflection.rb253
-rw-r--r--activerecord/lib/active_record/relation.rb156
-rw-r--r--activerecord/lib/active_record/relation/batches.rb12
-rw-r--r--activerecord/lib/active_record/relation/calculations.rb188
-rw-r--r--activerecord/lib/active_record/relation/finder_methods.rb110
-rw-r--r--activerecord/lib/active_record/relation/predicate_builder.rb32
-rw-r--r--activerecord/lib/active_record/relation/query_methods.rb314
-rw-r--r--activerecord/lib/active_record/relation/spawn_methods.rb115
-rw-r--r--activerecord/lib/active_record/result.rb34
-rw-r--r--activerecord/lib/active_record/schema.rb11
-rw-r--r--activerecord/lib/active_record/schema_dumper.rb26
-rw-r--r--activerecord/lib/active_record/serialization.rb8
-rw-r--r--activerecord/lib/active_record/serializers/xml_serializer.rb22
-rw-r--r--activerecord/lib/active_record/session_store.rb27
-rw-r--r--activerecord/lib/active_record/test_case.rb30
-rw-r--r--activerecord/lib/active_record/timestamp.rb65
-rw-r--r--activerecord/lib/active_record/transactions.rb49
-rw-r--r--activerecord/lib/active_record/validations.rb39
-rw-r--r--activerecord/lib/active_record/validations/associated.rb8
-rw-r--r--activerecord/lib/active_record/validations/uniqueness.rb88
-rw-r--r--activerecord/lib/active_record/version.rb6
108 files changed, 7399 insertions, 5530 deletions
diff --git a/activerecord/lib/active_record/aggregations.rb b/activerecord/lib/active_record/aggregations.rb
index 83a9ab46c5..90d3b58c78 100644
--- a/activerecord/lib/active_record/aggregations.rb
+++ b/activerecord/lib/active_record/aggregations.rb
@@ -4,17 +4,15 @@ module ActiveRecord
extend ActiveSupport::Concern
def clear_aggregation_cache #:nodoc:
- self.class.reflect_on_all_aggregations.to_a.each do |assoc|
- instance_variable_set "@#{assoc.name}", nil
- end unless self.new_record?
+ @aggregation_cache.clear if persisted?
end
- # Active Record implements aggregation through a macro-like class method called +composed_of+
- # for representing attributes as value objects. It expresses relationships like "Account [is]
- # composed of Money [among other things]" or "Person [is] composed of [an] address". Each call
- # to the macro adds a description of how the value objects are created from the attributes of
- # the entity object (when the entity is initialized either as a new object or from finding an
- # existing object) and how it can be turned back into attributes (when the entity is saved to
+ # Active Record implements aggregation through a macro-like class method called +composed_of+
+ # for representing attributes as value objects. It expresses relationships like "Account [is]
+ # composed of Money [among other things]" or "Person [is] composed of [an] address". Each call
+ # to the macro adds a description of how the value objects are created from the attributes of
+ # the entity object (when the entity is initialized either as a new object or from finding an
+ # existing object) and how it can be turned back into attributes (when the entity is saved to
# the database).
#
# class Customer < ActiveRecord::Base
@@ -70,9 +68,9 @@ module ActiveRecord
# end
# end
#
- # Now it's possible to access attributes from the database through the value objects instead. If
- # you choose to name the composition the same as the attribute's name, it will be the only way to
- # access that attribute. That's the case with our +balance+ attribute. You interact with the value
+ # Now it's possible to access attributes from the database through the value objects instead. If
+ # you choose to name the composition the same as the attribute's name, it will be the only way to
+ # access that attribute. That's the case with our +balance+ attribute. You interact with the value
# objects just like you would any other attribute, though:
#
# customer.balance = Money.new(20) # sets the Money value object and the attribute
@@ -82,8 +80,8 @@ module ActiveRecord
# customer.balance == Money.new(20) # => true
# customer.balance < Money.new(5) # => false
#
- # Value objects can also be composed of multiple attributes, such as the case of Address. The order
- # of the mappings will determine the order of the parameters.
+ # Value objects can also be composed of multiple attributes, such as the case of Address. The order
+ # of the mappings will determine the order of the parameters.
#
# customer.address_street = "Hyancintvej"
# customer.address_city = "Copenhagen"
@@ -94,42 +92,42 @@ module ActiveRecord
#
# == Writing value objects
#
- # Value objects are immutable and interchangeable objects that represent a given value, such as
- # a Money object representing $5. Two Money objects both representing $5 should be equal (through
- # methods such as <tt>==</tt> and <tt><=></tt> from Comparable if ranking makes sense). This is
+ # Value objects are immutable and interchangeable objects that represent a given value, such as
+ # a Money object representing $5. Two Money objects both representing $5 should be equal (through
+ # methods such as <tt>==</tt> and <tt><=></tt> from Comparable if ranking makes sense). This is
# unlike entity objects where equality is determined by identity. An entity class such as Customer can
- # easily have two different objects that both have an address on Hyancintvej. Entity identity is
- # determined by object or relational unique identifiers (such as primary keys). Normal
+ # easily have two different objects that both have an address on Hyancintvej. Entity identity is
+ # determined by object or relational unique identifiers (such as primary keys). Normal
# ActiveRecord::Base classes are entity objects.
#
- # It's also important to treat the value objects as immutable. Don't allow the Money object to have
- # its amount changed after creation. Create a new Money object with the new value instead. This
- # is exemplified by the Money#exchange_to method that returns a new value object instead of changing
- # its own values. Active Record won't persist value objects that have been changed through means
+ # It's also important to treat the value objects as immutable. Don't allow the Money object to have
+ # its amount changed after creation. Create a new Money object with the new value instead. This
+ # is exemplified by the Money#exchange_to method that returns a new value object instead of changing
+ # its own values. Active Record won't persist value objects that have been changed through means
# other than the writer method.
#
- # The immutable requirement is enforced by Active Record by freezing any object assigned as a value
+ # The immutable requirement is enforced by Active Record by freezing any object assigned as a value
# object. Attempting to change it afterwards will result in a ActiveSupport::FrozenObjectError.
#
- # Read more about value objects on http://c2.com/cgi/wiki?ValueObject and on the dangers of not
+ # Read more about value objects on http://c2.com/cgi/wiki?ValueObject and on the dangers of not
# keeping value objects immutable on http://c2.com/cgi/wiki?ValueObjectsShouldBeImmutable
#
# == Custom constructors and converters
#
- # By default value objects are initialized by calling the <tt>new</tt> constructor of the value
- # class passing each of the mapped attributes, in the order specified by the <tt>:mapping</tt>
- # option, as arguments. If the value class doesn't support this convention then +composed_of+ allows
+ # By default value objects are initialized by calling the <tt>new</tt> constructor of the value
+ # class passing each of the mapped attributes, in the order specified by the <tt>:mapping</tt>
+ # option, as arguments. If the value class doesn't support this convention then +composed_of+ allows
# a custom constructor to be specified.
#
- # When a new value is assigned to the value object the default assumption is that the new value
- # is an instance of the value class. Specifying a custom converter allows the new value to be automatically
+ # When a new value is assigned to the value object the default assumption is that the new value
+ # is an instance of the value class. Specifying a custom converter allows the new value to be automatically
# converted to an instance of value class if necessary.
#
- # For example, the NetworkResource model has +network_address+ and +cidr_range+ attributes that
- # should be aggregated using the NetAddr::CIDR value class (http://netaddr.rubyforge.org). The constructor
- # for the value class is called +create+ and it expects a CIDR address string as a parameter. New
- # values can be assigned to the value object using either another NetAddr::CIDR object, a string
- # or an array. The <tt>:constructor</tt> and <tt>:converter</tt> options can be used to meet
+ # For example, the NetworkResource model has +network_address+ and +cidr_range+ attributes that
+ # should be aggregated using the NetAddr::CIDR value class (http://netaddr.rubyforge.org). The constructor
+ # for the value class is called +create+ and it expects a CIDR address string as a parameter. New
+ # values can be assigned to the value object using either another NetAddr::CIDR object, a string
+ # or an array. The <tt>:constructor</tt> and <tt>:converter</tt> options can be used to meet
# these requirements:
#
# class NetworkResource < ActiveRecord::Base
@@ -157,38 +155,38 @@ module ActiveRecord
#
# == Finding records by a value object
#
- # Once a +composed_of+ relationship is specified for a model, records can be loaded from the database
- # by specifying an instance of the value object in the conditions hash. The following example
+ # Once a +composed_of+ relationship is specified for a model, records can be loaded from the database
+ # by specifying an instance of the value object in the conditions hash. The following example
# finds all customers with +balance_amount+ equal to 20 and +balance_currency+ equal to "USD":
#
- # Customer.find(:all, :conditions => {:balance => Money.new(20, "USD")})
+ # Customer.where(:balance => Money.new(20, "USD")).all
#
module ClassMethods
# Adds reader and writer methods for manipulating a value object:
# <tt>composed_of :address</tt> adds <tt>address</tt> and <tt>address=(new_address)</tt> methods.
#
# Options are:
- # * <tt>:class_name</tt> - Specifies the class name of the association. Use it only if that name
- # can't be inferred from the part id. So <tt>composed_of :address</tt> will by default be linked
- # to the Address class, but if the real class name is CompanyAddress, you'll have to specify it
+ # * <tt>:class_name</tt> - Specifies the class name of the association. Use it only if that name
+ # can't be inferred from the part id. So <tt>composed_of :address</tt> will by default be linked
+ # to the Address class, but if the real class name is CompanyAddress, you'll have to specify it
# with this option.
- # * <tt>:mapping</tt> - Specifies the mapping of entity attributes to attributes of the value
- # object. Each mapping is represented as an array where the first item is the name of the
- # entity attribute and the second item is the name the attribute in the value object. The
- # order in which mappings are defined determine the order in which attributes are sent to the
+ # * <tt>:mapping</tt> - Specifies the mapping of entity attributes to attributes of the value
+ # object. Each mapping is represented as an array where the first item is the name of the
+ # entity attribute and the second item is the name the attribute in the value object. The
+ # order in which mappings are defined determine the order in which attributes are sent to the
# value class constructor.
# * <tt>:allow_nil</tt> - Specifies that the value object will not be instantiated when all mapped
- # attributes are +nil+. Setting the value object to +nil+ has the effect of writing +nil+ to all
+ # attributes are +nil+. Setting the value object to +nil+ has the effect of writing +nil+ to all
# mapped attributes.
# This defaults to +false+.
- # * <tt>:constructor</tt> - A symbol specifying the name of the constructor method or a Proc that
- # is called to initialize the value object. The constructor is passed all of the mapped attributes,
- # in the order that they are defined in the <tt>:mapping option</tt>, as arguments and uses them
+ # * <tt>:constructor</tt> - A symbol specifying the name of the constructor method or a Proc that
+ # is called to initialize the value object. The constructor is passed all of the mapped attributes,
+ # in the order that they are defined in the <tt>:mapping option</tt>, as arguments and uses them
# to instantiate a <tt>:class_name</tt> object.
# The default is <tt>:new</tt>.
- # * <tt>:converter</tt> - A symbol specifying the name of a class method of <tt>:class_name</tt>
- # or a Proc that is called when a new value is assigned to the value object. The converter is
- # passed the single value that is used in the assignment and is only called if the new value is
+ # * <tt>:converter</tt> - A symbol specifying the name of a class method of <tt>:class_name</tt>
+ # or a Proc that is called when a new value is assigned to the value object. The converter is
+ # passed the single value that is used in the assignment and is only called if the new value is
# not an instance of <tt>:class_name</tt>.
#
# Option examples:
@@ -222,53 +220,32 @@ module ActiveRecord
private
def reader_method(name, class_name, mapping, allow_nil, constructor)
- module_eval do
- define_method(name) do |*args|
- force_reload = args.first || false
-
- unless instance_variable_defined?("@#{name}")
- instance_variable_set("@#{name}", nil)
- end
-
- if (instance_variable_get("@#{name}").nil? || force_reload) && (!allow_nil || mapping.any? {|pair| !read_attribute(pair.first).nil? })
- attrs = mapping.collect {|pair| read_attribute(pair.first)}
- object = case constructor
- when Symbol
- class_name.constantize.send(constructor, *attrs)
- when Proc, Method
- constructor.call(*attrs)
- else
- raise ArgumentError, 'Constructor must be a symbol denoting the constructor method to call or a Proc to be invoked.'
- end
- instance_variable_set("@#{name}", object)
- end
- instance_variable_get("@#{name}")
+ define_method(name) do
+ if @aggregation_cache[name].nil? && (!allow_nil || mapping.any? {|pair| !read_attribute(pair.first).nil? })
+ attrs = mapping.collect {|pair| read_attribute(pair.first)}
+ object = constructor.respond_to?(:call) ?
+ constructor.call(*attrs) :
+ class_name.constantize.send(constructor, *attrs)
+ @aggregation_cache[name] = object
end
+ @aggregation_cache[name]
end
-
end
def writer_method(name, class_name, mapping, allow_nil, converter)
- module_eval do
- define_method("#{name}=") do |part|
- if part.nil? && allow_nil
- mapping.each { |pair| self[pair.first] = nil }
- instance_variable_set("@#{name}", nil)
- else
- unless part.is_a?(class_name.constantize) || converter.nil?
- part = case converter
- when Symbol
- class_name.constantize.send(converter, part)
- when Proc, Method
- converter.call(part)
- else
- raise ArgumentError, 'Converter must be a symbol denoting the converter method to call or a Proc to be invoked.'
- end
- end
-
- mapping.each { |pair| self[pair.first] = part.send(pair.last) }
- instance_variable_set("@#{name}", part.freeze)
+ define_method("#{name}=") do |part|
+ if part.nil? && allow_nil
+ mapping.each { |pair| self[pair.first] = nil }
+ @aggregation_cache[name] = nil
+ else
+ unless part.is_a?(class_name.constantize) || converter.nil?
+ part = converter.respond_to?(:call) ?
+ converter.call(part) :
+ class_name.constantize.send(converter, part)
end
+
+ mapping.each { |pair| self[pair.first] = part.send(pair.last) }
+ @aggregation_cache[name] = part.freeze
end
end
end
diff --git a/activerecord/lib/active_record/association_preload.rb b/activerecord/lib/active_record/association_preload.rb
deleted file mode 100644
index 0f0fdc2e21..0000000000
--- a/activerecord/lib/active_record/association_preload.rb
+++ /dev/null
@@ -1,403 +0,0 @@
-require 'active_support/core_ext/array/wrap'
-require 'active_support/core_ext/enumerable'
-
-module ActiveRecord
- # See ActiveRecord::AssociationPreload::ClassMethods for documentation.
- module AssociationPreload #:nodoc:
- extend ActiveSupport::Concern
-
- # Implements the details of eager loading of Active Record associations.
- # Application developers should not use this module directly.
- #
- # <tt>ActiveRecord::Base</tt> is extended with this module. The source code in
- # <tt>ActiveRecord::Base</tt> references methods defined in this module.
- #
- # Note that 'eager loading' and 'preloading' are actually the same thing.
- # However, there are two different eager loading strategies.
- #
- # The first one is by using table joins. This was only strategy available
- # prior to Rails 2.1. Suppose that you have an Author model with columns
- # 'name' and 'age', and a Book model with columns 'name' and 'sales'. Using
- # this strategy, Active Record would try to retrieve all data for an author
- # and all of its books via a single query:
- #
- # SELECT * FROM authors
- # LEFT OUTER JOIN books ON authors.id = books.id
- # WHERE authors.name = 'Ken Akamatsu'
- #
- # However, this could result in many rows that contain redundant data. After
- # having received the first row, we already have enough data to instantiate
- # the Author object. In all subsequent rows, only the data for the joined
- # 'books' table is useful; the joined 'authors' data is just redundant, and
- # processing this redundant data takes memory and CPU time. The problem
- # quickly becomes worse and worse as the level of eager loading increases
- # (i.e. if Active Record is to eager load the associations' associations as
- # well).
- #
- # The second strategy is to use multiple database queries, one for each
- # level of association. Since Rails 2.1, this is the default strategy. In
- # situations where a table join is necessary (e.g. when the +:conditions+
- # option references an association's column), it will fallback to the table
- # join strategy.
- #
- # See also ActiveRecord::Associations::ClassMethods, which explains eager
- # loading in a more high-level (application developer-friendly) manner.
- module ClassMethods
- protected
-
- # Eager loads the named associations for the given Active Record record(s).
- #
- # In this description, 'association name' shall refer to the name passed
- # to an association creation method. For example, a model that specifies
- # <tt>belongs_to :author</tt>, <tt>has_many :buyers</tt> has association
- # names +:author+ and +:buyers+.
- #
- # == Parameters
- # +records+ is an array of ActiveRecord::Base. This array needs not be flat,
- # i.e. +records+ itself may also contain arrays of records. In any case,
- # +preload_associations+ will preload the all associations records by
- # flattening +records+.
- #
- # +associations+ specifies one or more associations that you want to
- # preload. It may be:
- # - a Symbol or a String which specifies a single association name. For
- # example, specifying +:books+ allows this method to preload all books
- # for an Author.
- # - an Array which specifies multiple association names. This array
- # is processed recursively. For example, specifying <tt>[:avatar, :books]</tt>
- # allows this method to preload an author's avatar as well as all of his
- # books.
- # - a Hash which specifies multiple association names, as well as
- # association names for the to-be-preloaded association objects. For
- # example, specifying <tt>{ :author => :avatar }</tt> will preload a
- # book's author, as well as that author's avatar.
- #
- # +:associations+ has the same format as the +:include+ option for
- # <tt>ActiveRecord::Base.find</tt>. So +associations+ could look like this:
- #
- # :books
- # [ :books, :author ]
- # { :author => :avatar }
- # [ :books, { :author => :avatar } ]
- #
- # +preload_options+ contains options that will be passed to ActiveRecord::Base#find
- # (which is called under the hood for preloading records). But it is passed
- # only one level deep in the +associations+ argument, i.e. it's not passed
- # to the child associations when +associations+ is a Hash.
- def preload_associations(records, associations, preload_options={})
- records = Array.wrap(records).compact.uniq
- return if records.empty?
- case associations
- when Array then associations.each {|association| preload_associations(records, association, preload_options)}
- when Symbol, String then preload_one_association(records, associations.to_sym, preload_options)
- when Hash then
- associations.each do |parent, child|
- raise "parent must be an association name" unless parent.is_a?(String) || parent.is_a?(Symbol)
- preload_associations(records, parent, preload_options)
- reflection = reflections[parent]
- parents = records.sum { |record| Array.wrap(record.send(reflection.name)) }
- unless parents.empty?
- parents.first.class.preload_associations(parents, child)
- end
- end
- end
- end
-
- private
-
- # Preloads a specific named association for the given records. This is
- # called by +preload_associations+ as its base case.
- def preload_one_association(records, association, preload_options={})
- class_to_reflection = {}
- # Not all records have the same class, so group then preload
- # group on the reflection itself so that if various subclass share the same association then
- # we do not split them unnecessarily
- records.group_by { |record| class_to_reflection[record.class] ||= record.class.reflections[association]}.each do |reflection, _records|
- raise ConfigurationError, "Association named '#{ association }' was not found; perhaps you misspelled it?" unless reflection
-
- # 'reflection.macro' can return 'belongs_to', 'has_many', etc. Thus,
- # the following could call 'preload_belongs_to_association',
- # 'preload_has_many_association', etc.
- send("preload_#{reflection.macro}_association", _records, reflection, preload_options)
- end
- end
-
- def add_preloaded_records_to_collection(parent_records, reflection_name, associated_record)
- parent_records.each do |parent_record|
- association_proxy = parent_record.send(reflection_name)
- association_proxy.loaded
- association_proxy.target.push(*Array.wrap(associated_record))
-
- association_proxy.__send__(:set_inverse_instance, associated_record, parent_record)
- end
- end
-
- def add_preloaded_record_to_collection(parent_records, reflection_name, associated_record)
- parent_records.each do |parent_record|
- parent_record.send("set_#{reflection_name}_target", associated_record)
- end
- end
-
- def set_association_collection_records(id_to_record_map, reflection_name, associated_records, key)
- associated_records.each do |associated_record|
- mapped_records = id_to_record_map[associated_record[key].to_s]
- add_preloaded_records_to_collection(mapped_records, reflection_name, associated_record)
- end
- end
-
- def set_association_single_records(id_to_record_map, reflection_name, associated_records, key)
- seen_keys = {}
- associated_records.each do |associated_record|
- #this is a has_one or belongs_to: there should only be one record.
- #Unfortunately we can't (in portable way) ask the database for
- #'all records where foo_id in (x,y,z), but please
- # only one row per distinct foo_id' so this where we enforce that
- next if seen_keys[associated_record[key].to_s]
- seen_keys[associated_record[key].to_s] = true
- mapped_records = id_to_record_map[associated_record[key].to_s]
- mapped_records.each do |mapped_record|
- association_proxy = mapped_record.send("set_#{reflection_name}_target", associated_record)
- association_proxy.__send__(:set_inverse_instance, associated_record, mapped_record)
- end
- end
-
- id_to_record_map.each do |id, records|
- next if seen_keys.include?(id.to_s)
- records.each {|record| record.send("set_#{reflection_name}_target", nil) }
- end
- end
-
- # Given a collection of Active Record objects, constructs a Hash which maps
- # the objects' IDs to the relevant objects. Returns a 2-tuple
- # <tt>(id_to_record_map, ids)</tt> where +id_to_record_map+ is the Hash,
- # and +ids+ is an Array of record IDs.
- def construct_id_map(records, primary_key=nil)
- id_to_record_map = {}
- ids = []
- records.each do |record|
- primary_key ||= record.class.primary_key
- ids << record[primary_key]
- mapped_records = (id_to_record_map[ids.last.to_s] ||= [])
- mapped_records << record
- end
- ids.uniq!
- return id_to_record_map, ids
- end
-
- def preload_has_and_belongs_to_many_association(records, reflection, preload_options={})
- table_name = reflection.klass.quoted_table_name
- id_to_record_map, ids = construct_id_map(records)
- records.each {|record| record.send(reflection.name).loaded}
- options = reflection.options
-
- conditions = "t0.#{reflection.primary_key_name} #{in_or_equals_for_ids(ids)}"
- conditions << append_conditions(reflection, preload_options)
-
- associated_records = reflection.klass.unscoped.where([conditions, ids]).
- includes(options[:include]).
- joins("INNER JOIN #{connection.quote_table_name options[:join_table]} t0 ON #{reflection.klass.quoted_table_name}.#{reflection.klass.primary_key} = t0.#{reflection.association_foreign_key}").
- select("#{options[:select] || table_name+'.*'}, t0.#{reflection.primary_key_name} as the_parent_record_id").
- order(options[:order]).to_a
-
- set_association_collection_records(id_to_record_map, reflection.name, associated_records, 'the_parent_record_id')
- end
-
- def preload_has_one_association(records, reflection, preload_options={})
- return if records.first.send("loaded_#{reflection.name}?")
- id_to_record_map, ids = construct_id_map(records, reflection.options[:primary_key])
- options = reflection.options
- records.each {|record| record.send("set_#{reflection.name}_target", nil)}
- if options[:through]
- through_records = preload_through_records(records, reflection, options[:through])
- through_reflection = reflections[options[:through]]
- through_primary_key = through_reflection.primary_key_name
- unless through_records.empty?
- source = reflection.source_reflection.name
- through_records.first.class.preload_associations(through_records, source)
- if through_reflection.macro == :belongs_to
- rev_id_to_record_map, rev_ids = construct_id_map(records, through_primary_key)
- rev_primary_key = through_reflection.klass.primary_key
- through_records.each do |through_record|
- add_preloaded_record_to_collection(rev_id_to_record_map[through_record[rev_primary_key].to_s],
- reflection.name, through_record.send(source))
- end
- else
- through_records.each do |through_record|
- add_preloaded_record_to_collection(id_to_record_map[through_record[through_primary_key].to_s],
- reflection.name, through_record.send(source))
- end
- end
- end
- else
- set_association_single_records(id_to_record_map, reflection.name, find_associated_records(ids, reflection, preload_options), reflection.primary_key_name)
- end
- end
-
- def preload_has_many_association(records, reflection, preload_options={})
- return if records.first.send(reflection.name).loaded?
- options = reflection.options
-
- primary_key_name = reflection.through_reflection_primary_key_name
- id_to_record_map, ids = construct_id_map(records, primary_key_name || reflection.options[:primary_key])
- records.each {|record| record.send(reflection.name).loaded}
-
- if options[:through]
- through_records = preload_through_records(records, reflection, options[:through])
- through_reflection = reflections[options[:through]]
- unless through_records.empty?
- source = reflection.source_reflection.name
- through_records.first.class.preload_associations(through_records, source, options)
- through_records.each do |through_record|
- through_record_id = through_record[reflection.through_reflection_primary_key].to_s
- add_preloaded_records_to_collection(id_to_record_map[through_record_id], reflection.name, through_record.send(source))
- end
- end
-
- else
- set_association_collection_records(id_to_record_map, reflection.name, find_associated_records(ids, reflection, preload_options),
- reflection.primary_key_name)
- end
- end
-
- def preload_through_records(records, reflection, through_association)
- through_reflection = reflections[through_association]
- through_primary_key = through_reflection.primary_key_name
-
- through_records = []
- if reflection.options[:source_type]
- interface = reflection.source_reflection.options[:foreign_type]
- preload_options = {:conditions => ["#{connection.quote_column_name interface} = ?", reflection.options[:source_type]]}
-
- records.compact!
- records.first.class.preload_associations(records, through_association, preload_options)
-
- # Dont cache the association - we would only be caching a subset
- records.each do |record|
- proxy = record.send(through_association)
-
- if proxy.respond_to?(:target)
- through_records.concat Array.wrap(proxy.target)
- proxy.reset
- else # this is a has_one :through reflection
- through_records << proxy if proxy
- end
- end
- else
- options = {}
- options[:include] = reflection.options[:include] || reflection.options[:source] if reflection.options[:conditions]
- options[:order] = reflection.options[:order]
- options[:conditions] = reflection.options[:conditions]
- records.first.class.preload_associations(records, through_association, options)
-
- records.each do |record|
- through_records.concat Array.wrap(record.send(through_association))
- end
- end
- through_records
- end
-
- def preload_belongs_to_association(records, reflection, preload_options={})
- return if records.first.send("loaded_#{reflection.name}?")
- options = reflection.options
- primary_key_name = reflection.primary_key_name
-
- if options[:polymorphic]
- polymorph_type = options[:foreign_type]
- klasses_and_ids = {}
-
- # Construct a mapping from klass to a list of ids to load and a mapping of those ids back
- # to their parent_records
- records.each do |record|
- if klass = record.send(polymorph_type)
- klass_id = record.send(primary_key_name)
- if klass_id
- id_map = klasses_and_ids[klass] ||= {}
- id_list_for_klass_id = (id_map[klass_id.to_s] ||= [])
- id_list_for_klass_id << record
- end
- end
- end
- klasses_and_ids = klasses_and_ids.to_a
- else
- id_map = {}
- records.each do |record|
- key = record.send(primary_key_name)
- if key
- mapped_records = (id_map[key.to_s] ||= [])
- mapped_records << record
- end
- end
- klasses_and_ids = [[reflection.klass.name, id_map]]
- end
-
- klasses_and_ids.each do |klass_and_id|
- klass_name, id_map = *klass_and_id
- next if id_map.empty?
- klass = klass_name.constantize
-
- table_name = klass.quoted_table_name
- primary_key = reflection.options[:primary_key] || klass.primary_key
- column_type = klass.columns.detect{|c| c.name == primary_key}.type
-
- ids = id_map.keys.map do |id|
- if column_type == :integer
- id.to_i
- elsif column_type == :float
- id.to_f
- else
- id
- end
- end
-
- conditions = "#{table_name}.#{connection.quote_column_name(primary_key)} #{in_or_equals_for_ids(ids)}"
- conditions << append_conditions(reflection, preload_options)
-
- associated_records = klass.unscoped.where([conditions, ids]).apply_finder_options(options.slice(:include, :select, :joins, :order)).to_a
-
- set_association_single_records(id_map, reflection.name, associated_records, primary_key)
- end
- end
-
- def find_associated_records(ids, reflection, preload_options)
- options = reflection.options
- table_name = reflection.klass.quoted_table_name
-
- if interface = reflection.options[:as]
- conditions = "#{reflection.klass.quoted_table_name}.#{connection.quote_column_name "#{interface}_id"} #{in_or_equals_for_ids(ids)} and #{reflection.klass.quoted_table_name}.#{connection.quote_column_name "#{interface}_type"} = '#{self.base_class.sti_name}'"
- else
- foreign_key = reflection.primary_key_name
- conditions = "#{reflection.klass.quoted_table_name}.#{foreign_key} #{in_or_equals_for_ids(ids)}"
- end
-
- conditions << append_conditions(reflection, preload_options)
-
- find_options = {
- :select => preload_options[:select] || options[:select] || "#{table_name}.*",
- :include => preload_options[:include] || options[:include],
- :conditions => [conditions, ids],
- :joins => options[:joins],
- :group => preload_options[:group] || options[:group],
- :order => preload_options[:order] || options[:order]
- }
-
- reflection.klass.scoped.apply_finder_options(find_options).to_a
- end
-
-
- def interpolate_sql_for_preload(sql)
- instance_eval("%@#{sql.gsub('@', '\@')}@", __FILE__, __LINE__)
- end
-
- def append_conditions(reflection, preload_options)
- sql = ""
- sql << " AND (#{interpolate_sql_for_preload(reflection.sanitized_conditions)})" if reflection.sanitized_conditions
- sql << " AND (#{sanitize_sql preload_options[:conditions]})" if preload_options[:conditions]
- sql
- end
-
- def in_or_equals_for_ids(ids)
- ids.size > 1 ? "IN (?)" : "= ?"
- end
- end
- end
-end
diff --git a/activerecord/lib/active_record/associations.rb b/activerecord/lib/active_record/associations.rb
index 73c0900c8b..08fb6bf3c4 100644
--- a/activerecord/lib/active_record/associations.rb
+++ b/activerecord/lib/active_record/associations.rb
@@ -4,6 +4,7 @@ require 'active_support/core_ext/module/delegation'
require 'active_support/core_ext/object/blank'
require 'active_support/core_ext/string/conversions'
require 'active_support/core_ext/module/remove_method'
+require 'active_support/core_ext/class/attribute'
module ActiveRecord
class InverseOfAssociationNotFoundError < ActiveRecordError #:nodoc:
@@ -18,32 +19,36 @@ module ActiveRecord
end
end
- class HasManyThroughAssociationPolymorphicError < ActiveRecordError #:nodoc:
+ class HasManyThroughAssociationPolymorphicSourceError < ActiveRecordError #:nodoc:
def initialize(owner_class_name, reflection, source_reflection)
super("Cannot have a has_many :through association '#{owner_class_name}##{reflection.name}' on the polymorphic object '#{source_reflection.class_name}##{source_reflection.name}'.")
end
end
+ class HasManyThroughAssociationPolymorphicThroughError < ActiveRecordError #:nodoc:
+ def initialize(owner_class_name, reflection)
+ super("Cannot have a has_many :through association '#{owner_class_name}##{reflection.name}' which goes through the polymorphic association '#{owner_class_name}##{reflection.through_reflection.name}'.")
+ end
+ end
+
class HasManyThroughAssociationPointlessSourceTypeError < ActiveRecordError #:nodoc:
def initialize(owner_class_name, reflection, source_reflection)
super("Cannot have a has_many :through association '#{owner_class_name}##{reflection.name}' with a :source_type option if the '#{reflection.through_reflection.class_name}##{source_reflection.name}' is not polymorphic. Try removing :source_type on your association.")
end
end
+ class HasOneThroughCantAssociateThroughCollection < ActiveRecordError #:nodoc:
+ def initialize(owner_class_name, reflection, through_reflection)
+ super("Cannot have a has_one :through association '#{owner_class_name}##{reflection.name}' where the :through association '#{owner_class_name}##{through_reflection.name}' is a collection. Specify a has_one or belongs_to association in the :through option instead.")
+ end
+ end
+
class HasManyThroughSourceAssociationNotFoundError < ActiveRecordError #:nodoc:
def initialize(reflection)
through_reflection = reflection.through_reflection
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 ', :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
-
- class HasManyThroughSourceAssociationMacroError < ActiveRecordError #:nodoc:
- def initialize(reflection)
- through_reflection = reflection.through_reflection
- source_reflection = reflection.source_reflection
- super("Invalid source reflection macro :#{source_reflection.macro}#{" :through" if source_reflection.options[:through]} for has_many #{reflection.name.inspect}, :through => #{through_reflection.name.inspect}. Use :source to specify the source reflection.")
+ super("Could not find the source association(s) #{source_reflection_names.collect{ |a| a.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
@@ -65,6 +70,12 @@ module ActiveRecord
end
end
+ class HasManyThroughNestedAssociationsAreReadonly < ActiveRecordError #:nodoc
+ def initialize(owner, reflection)
+ super("Cannot modify association '#{owner.class.name}##{reflection.name}' because it goes through more than one other association.")
+ end
+ end
+
class HasAndBelongsToManyAssociationWithPrimaryKeyError < ActiveRecordError #:nodoc:
def initialize(reflection)
super("Primary key is not allowed in a has_and_belongs_to_many join table (#{reflection.options[:join_table]}).")
@@ -93,8 +104,8 @@ module ActiveRecord
# (has_many, has_one) when there is at least 1 child associated instance.
# ex: if @project.tasks.size > 0, DeleteRestrictionError will be raised when trying to destroy @project
class DeleteRestrictionError < ActiveRecordError #:nodoc:
- def initialize(reflection)
- super("Cannot delete record because of dependent #{reflection.name}")
+ def initialize(name)
+ super("Cannot delete record because of dependent #{name}")
end
end
@@ -104,44 +115,75 @@ module ActiveRecord
# These classes will be loaded when associations are created.
# So there is no need to eager load them.
- autoload :AssociationCollection, 'active_record/associations/association_collection'
- autoload :AssociationProxy, 'active_record/associations/association_proxy'
- autoload :BelongsToAssociation, 'active_record/associations/belongs_to_association'
+ autoload :Association, 'active_record/associations/association'
+ autoload :SingularAssociation, 'active_record/associations/singular_association'
+ autoload :CollectionAssociation, 'active_record/associations/collection_association'
+ autoload :CollectionProxy, 'active_record/associations/collection_proxy'
+
+ autoload :BelongsToAssociation, 'active_record/associations/belongs_to_association'
autoload :BelongsToPolymorphicAssociation, 'active_record/associations/belongs_to_polymorphic_association'
- autoload :HasAndBelongsToManyAssociation, 'active_record/associations/has_and_belongs_to_many_association'
- autoload :HasManyAssociation, 'active_record/associations/has_many_association'
- autoload :HasManyThroughAssociation, 'active_record/associations/has_many_through_association'
- autoload :HasOneAssociation, 'active_record/associations/has_one_association'
- autoload :HasOneThroughAssociation, 'active_record/associations/has_one_through_association'
+ autoload :HasAndBelongsToManyAssociation, 'active_record/associations/has_and_belongs_to_many_association'
+ autoload :HasManyAssociation, 'active_record/associations/has_many_association'
+ autoload :HasManyThroughAssociation, 'active_record/associations/has_many_through_association'
+ autoload :HasOneAssociation, 'active_record/associations/has_one_association'
+ autoload :HasOneThroughAssociation, 'active_record/associations/has_one_through_association'
+ autoload :ThroughAssociation, 'active_record/associations/through_association'
+
+ module Builder #:nodoc:
+ autoload :Association, 'active_record/associations/builder/association'
+ autoload :SingularAssociation, 'active_record/associations/builder/singular_association'
+ autoload :CollectionAssociation, 'active_record/associations/builder/collection_association'
+
+ autoload :BelongsTo, 'active_record/associations/builder/belongs_to'
+ autoload :HasOne, 'active_record/associations/builder/has_one'
+ autoload :HasMany, 'active_record/associations/builder/has_many'
+ autoload :HasAndBelongsToMany, 'active_record/associations/builder/has_and_belongs_to_many'
+ end
+
+ autoload :Preloader, 'active_record/associations/preloader'
+ autoload :JoinDependency, 'active_record/associations/join_dependency'
+ autoload :AssociationScope, 'active_record/associations/association_scope'
+ autoload :AliasTracker, 'active_record/associations/alias_tracker'
+ autoload :JoinHelper, 'active_record/associations/join_helper'
# Clears out the association cache.
def clear_association_cache #:nodoc:
- self.class.reflect_on_all_associations.to_a.each do |assoc|
- instance_variable_set "@#{assoc.name}", nil
- end unless self.new_record?
+ @association_cache.clear if persisted?
+ end
+
+ # :nodoc:
+ attr_reader :association_cache
+
+ # Returns the association instance for the given name, instantiating it if it doesn't already exist
+ def association(name) #:nodoc:
+ association = association_instance_get(name)
+
+ if association.nil?
+ reflection = self.class.reflect_on_association(name)
+ association = reflection.association_class.new(self, reflection)
+ association_instance_set(name, association)
+ end
+
+ association
end
private
# Returns the specified association instance if it responds to :loaded?, nil otherwise.
def association_instance_get(name)
- ivar = "@#{name}"
- if instance_variable_defined?(ivar)
- association = instance_variable_get(ivar)
- association if association.respond_to?(:loaded?)
- end
+ @association_cache[name.to_sym]
end
# Set the specified association instance.
def association_instance_set(name, association)
- instance_variable_set("@#{name}", association)
+ @association_cache[name] = association
end
- # Associations are a set of macro-like class methods for tying objects together through
- # 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
+ # 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>
- # methods.
+ # methods.
#
# class Project < ActiveRecord::Base
# belongs_to :portfolio
@@ -150,7 +192,7 @@ module ActiveRecord
# has_and_belongs_to_many :categories
# end
#
- # The project class now has the following methods (and more) to ease the traversal and
+ # The project class now has the following methods (and more) to ease the traversal and
# manipulation of its relationships:
# * <tt>Project#portfolio, Project#portfolio=(portfolio), Project#portfolio.nil?</tt>
# * <tt>Project#project_manager, Project#project_manager=(project_manager), Project#project_manager.nil?,</tt>
@@ -162,8 +204,8 @@ module ActiveRecord
#
# === A word of warning
#
- # Don't create associations that have the same name as instance methods of
- # <tt>ActiveRecord::Base</tt>. Since the association adds a method with that name to
+ # Don't create associations that have the same name as instance methods of
+ # <tt>ActiveRecord::Base</tt>. Since the association adds a method with that name to
# its model, it will override the inherited method and break things.
# For instance, +attributes+ and +connection+ would be bad choices for association names.
#
@@ -177,7 +219,7 @@ module ActiveRecord
# other=(other) | X | X | X
# build_other(attributes={}) | X | | X
# create_other(attributes={}) | X | | X
- # other.create!(attributes={}) | | | X
+ # create_other!(attributes={}) | X | | X
#
# ===Collection associations (one-to-many / many-to-many)
# | | | has_many
@@ -200,10 +242,9 @@ module ActiveRecord
# others.empty? | X | X | X
# others.clear | X | X | X
# others.delete(other,other,...) | X | X | X
- # others.delete_all | X | X |
+ # others.delete_all | X | X | X
# others.destroy_all | X | X | X
# others.find(*args) | X | X | X
- # others.find_first | X | |
# others.exists? | X | X | X
# others.uniq | X | X | X
# others.reset | X | X | X
@@ -274,7 +315,7 @@ module ActiveRecord
#
# == Is it a +belongs_to+ or +has_one+ association?
#
- # Both express a 1-1 relationship. The difference is mostly where to place the foreign
+ # Both express a 1-1 relationship. The difference is mostly where to place the foreign
# key, which goes on the table for the class declaring the +belongs_to+ relationship.
#
# class User < ActiveRecord::Base
@@ -304,43 +345,49 @@ module ActiveRecord
#
# == Unsaved objects and associations
#
- # 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
+ # 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.
#
# You can set 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. Setting it
# to +true+ will _always_ save the members, whereas setting it to +false+ will
- # _never_ save the members.
+ # _never_ save the members. More details about :autosave option is available at
+ # autosave_association.rb .
#
# === One-to-one associations
#
- # * Assigning an object to a +has_one+ association automatically saves that object and
- # the object being replaced (if there is one), in order to update their primary
- # keys - except if the parent object is unsaved (<tt>new_record? == true</tt>).
- # * If either of these saves fail (due to one of the objects being invalid) the assignment
- # statement returns +false+ and the assignment is cancelled.
- # * If you wish to assign an object to a +has_one+ association without saving it,
- # use the <tt>association.build</tt> method (documented below).
- # * Assigning an object to a +belongs_to+ association does not save the object, since
- # the foreign key field belongs on the parent. It does not save the parent either.
+ # * Assigning an object to a +has_one+ association automatically saves that object and
+ # the object being replaced (if there is one), in order to update their foreign
+ # keys - except if the parent object is unsaved (<tt>new_record? == true</tt>).
+ # * If either of these saves fail (due to one of the objects being invalid), an
+ # <tt>ActiveRecord::RecordNotSaved</tt> exception is raised and the assignment is
+ # cancelled.
+ # * If you wish to assign an object to a +has_one+ association without saving it,
+ # use the <tt>build_association</tt> method (documented below). The object being
+ # replaced will still be saved to update its foreign key.
+ # * Assigning an object to a +belongs_to+ association does not save the object, since
+ # the foreign key field belongs on the parent. It does not save the parent either.
#
# === Collections
#
- # * Adding an object to a collection (+has_many+ or +has_and_belongs_to_many+) automatically
- # saves that object, except if the parent object (the owner of the collection) is not yet
- # stored in the database.
- # * If saving any of the objects being added to a collection (via <tt>push</tt> or similar)
- # fails, then <tt>push</tt> returns +false+.
- # * You can add an object to a collection without automatically saving it by using the
- # <tt>collection.build</tt> method (documented below).
- # * All unsaved (<tt>new_record? == true</tt>) members of the collection are automatically
- # saved when the parent is saved.
+ # * Adding an object to a collection (+has_many+ or +has_and_belongs_to_many+) automatically
+ # saves that object, except if the parent object (the owner of the collection) is not yet
+ # stored in the database.
+ # * If saving any of the objects being added to a collection (via <tt>push</tt> or similar)
+ # fails, then <tt>push</tt> returns +false+.
+ # * If saving fails while replacing the collection (via <tt>association=</tt>), an
+ # <tt>ActiveRecord::RecordNotSaved</tt> exception is raised and the assignment is
+ # cancelled.
+ # * You can add an object to a collection without automatically saving it by using the
+ # <tt>collection.build</tt> method (documented below).
+ # * All unsaved (<tt>new_record? == true</tt>) members of the collection are automatically
+ # saved when the parent is saved.
#
# === Association callbacks
#
- # Similar to the normal callbacks that hook into the lifecycle of an Active Record object,
- # you can also define callbacks that get triggered when you add an object to or remove an
+ # Similar to the normal callbacks that hook into the life cycle of an Active Record object,
+ # you can also define callbacks that get triggered when you add an object to or remove an
# object from an association collection.
#
# class Project
@@ -354,20 +401,20 @@ module ActiveRecord
# It's possible to stack callbacks by passing them as an array. Example:
#
# class Project
- # has_and_belongs_to_many :developers,
+ # has_and_belongs_to_many :developers,
# :after_add => [:evaluate_velocity, Proc.new { |p, d| p.shipping_date = Time.now}]
# end
#
# Possible callbacks are: +before_add+, +after_add+, +before_remove+ and +after_remove+.
#
- # Should any of the +before_add+ callbacks throw an exception, the object does not get
- # added to the collection. Same with the +before_remove+ callbacks; if an exception is
+ # Should any of the +before_add+ callbacks throw an exception, the object does not get
+ # added to the collection. Same with the +before_remove+ callbacks; if an exception is
# thrown the object doesn't get removed.
#
# === Association extensions
#
- # The proxy objects that control the access to associations can be extended through anonymous
- # modules. This is especially beneficial for adding new finders, creators, and other
+ # The proxy objects that control the access to associations can be extended through anonymous
+ # modules. This is especially beneficial for adding new finders, creators, and other
# factory-type methods that are only used as part of this association.
#
# class Account < ActiveRecord::Base
@@ -383,8 +430,8 @@ module ActiveRecord
# person.first_name # => "David"
# person.last_name # => "Heinemeier Hansson"
#
- # If you need to share the same extensions between many associations, you can use a named
- # extension module.
+ # If you need to share the same extensions between many associations, you can use a named
+ # extension module.
#
# module FindOrCreateByNameExtension
# def find_or_create_by_name(name)
@@ -401,10 +448,10 @@ module ActiveRecord
# has_many :people, :extend => FindOrCreateByNameExtension
# end
#
- # If you need to use multiple named extension modules, you can specify an array of modules
+ # If you need to use multiple named extension modules, you can specify an array of modules
# with the <tt>:extend</tt> option.
- # In the case of name conflicts between methods in the modules, methods in modules later
- # in the array supercede those earlier in the array.
+ # In the case of name conflicts between methods in the modules, methods in modules later
+ # in the array supercede those earlier in the array.
#
# class Account < ActiveRecord::Base
# has_many :people, :extend => [FindOrCreateByNameExtension, FindRecentExtension]
@@ -415,13 +462,13 @@ module ActiveRecord
#
# * +proxy_owner+ - Returns the object the association is part of.
# * +proxy_reflection+ - Returns the reflection object that describes the association.
- # * +proxy_target+ - Returns the associated object for +belongs_to+ and +has_one+, or
+ # * +proxy_target+ - Returns the associated object for +belongs_to+ and +has_one+, or
# the collection of associated objects for +has_many+ and +has_and_belongs_to_many+.
#
# === Association Join Models
#
- # Has Many associations can be configured with the <tt>:through</tt> option to use an
- # explicit join model to retrieve the data. This operates similarly to a
+ # Has Many associations can be configured with the <tt>:through</tt> option to use an
+ # explicit join model to retrieve the data. This operates similarly to a
# +has_and_belongs_to_many+ association. The advantage is that you're able to add validations,
# callbacks, and extra attributes on the join model. Consider the following schema:
#
@@ -479,17 +526,76 @@ module ActiveRecord
# @group.users.collect { |u| u.avatar }.flatten # select all avatars for all users in the group
# @group.avatars # selects all avatars by going through the User join model.
#
- # An important caveat with going through +has_one+ or +has_many+ associations on the
- # join model is that these associations are *read-only*. For example, the following
+ # An important caveat with going through +has_one+ or +has_many+ associations on the
+ # join model is that these associations are *read-only*. For example, the following
# would not work following the previous example:
#
# @group.avatars << Avatar.new # this would work if User belonged_to Avatar rather than the other way around
# @group.avatars.delete(@group.avatars.last) # so would this
#
+ # If you are using a +belongs_to+ on the join model, it is a good idea to set the
+ # <tt>:inverse_of</tt> option on the +belongs_to+, which will mean that the following example
+ # works correctly (where <tt>tags</tt> is a +has_many+ <tt>:through</tt> association):
+ #
+ # @post = Post.first
+ # @tag = @post.tags.build :name => "ruby"
+ # @tag.save
+ #
+ # The last line ought to save the through record (a <tt>Taggable</tt>). This will only work if the
+ # <tt>:inverse_of</tt> is set:
+ #
+ # class Taggable < ActiveRecord::Base
+ # belongs_to :post
+ # belongs_to :tag, :inverse_of => :taggings
+ # end
+ #
+ # === Nested Associations
+ #
+ # You can actually specify *any* association with the <tt>:through</tt> option, including an
+ # association which has a <tt>:through</tt> option itself. For example:
+ #
+ # class Author < ActiveRecord::Base
+ # has_many :posts
+ # has_many :comments, :through => :posts
+ # has_many :commenters, :through => :comments
+ # end
+ #
+ # class Post < ActiveRecord::Base
+ # has_many :comments
+ # end
+ #
+ # class Comment < ActiveRecord::Base
+ # belongs_to :commenter
+ # end
+ #
+ # @author = Author.first
+ # @author.commenters # => People who commented on posts written by the author
+ #
+ # An equivalent way of setting up this association this would be:
+ #
+ # class Author < ActiveRecord::Base
+ # has_many :posts
+ # has_many :commenters, :through => :posts
+ # end
+ #
+ # class Post < ActiveRecord::Base
+ # has_many :comments
+ # has_many :commenters, :through => :comments
+ # end
+ #
+ # class Comment < ActiveRecord::Base
+ # belongs_to :commenter
+ # end
+ #
+ # When using nested association, you will not be able to modify the association because there
+ # is not enough information to know what modification to make. For example, if you tried to
+ # add a <tt>Commenter</tt> in the example above, there would be no way to tell how to set up the
+ # intermediate <tt>Post</tt> and <tt>Comment</tt> objects.
+ #
# === Polymorphic Associations
#
- # Polymorphic associations on models are not restricted on what types of models they
- # can be associated with. Rather, they specify an interface that a +has_many+ association
+ # Polymorphic associations on models are not restricted on what types of models they
+ # can be associated with. Rather, they specify an interface that a +has_many+ association
# must adhere to.
#
# class Asset < ActiveRecord::Base
@@ -502,15 +608,15 @@ module ActiveRecord
#
# @asset.attachable = @post
#
- # This works by using a type column in addition to a foreign key to specify the associated
- # record. In the Asset example, you'd need an +attachable_id+ integer column and an
+ # This works by using a type column in addition to a foreign key to specify the associated
+ # record. In the Asset example, you'd need an +attachable_id+ integer column and an
# +attachable_type+ string column.
#
- # Using polymorphic associations in combination with single table inheritance (STI) is
- # a little tricky. In order for the associations to work as expected, ensure that you
- # store the base model for the STI models in the type column of the polymorphic
+ # Using polymorphic associations in combination with single table inheritance (STI) is
+ # a little tricky. In order for the associations to work as expected, ensure that you
+ # store the base model for the STI models in the type column of the polymorphic
# association. To continue with the asset example above, suppose there are guest posts
- # and member posts that use the posts table for STI. In this case, there must be a +type+
+ # and member posts that use the posts table for STI. In this case, there must be a +type+
# column in the posts table.
#
# class Asset < ActiveRecord::Base
@@ -534,8 +640,8 @@ module ActiveRecord
#
# == Caching
#
- # All of the methods are built on a simple caching principle that will keep the result
- # of the last query around unless specifically instructed not to. The cache is even
+ # All of the methods are built on a simple caching principle that will keep the result
+ # of the last query around unless specifically instructed not to. The cache is even
# shared across methods to make it even cheaper to use the macro-added methods without
# worrying too much about performance at the first go.
#
@@ -547,9 +653,9 @@ module ActiveRecord
#
# == Eager loading of associations
#
- # Eager loading is a way to find objects of a certain class and a number of named associations.
- # This is one of the easiest ways of to prevent the dreaded 1+N problem in which fetching 100
- # posts that each need to display their author triggers 101 database queries. Through the
+ # Eager loading is a way to find objects of a certain class and a number of named associations.
+ # This is one of the easiest ways of to prevent the dreaded 1+N problem in which fetching 100
+ # posts that each need to display their author triggers 101 database queries. Through the
# use of eager loading, the 101 queries can be reduced to 2.
#
# class Post < ActiveRecord::Base
@@ -565,54 +671,54 @@ module ActiveRecord
# puts "Last comment on: " + post.comments.first.created_on
# end
#
- # To iterate over these one hundred posts, we'll generate 201 database queries. Let's
+ # To iterate over these one hundred posts, we'll generate 201 database queries. Let's
# first just optimize it for retrieving the author:
#
# for post in Post.find(:all, :include => :author)
#
- # This references the name of the +belongs_to+ association that also used the <tt>:author</tt>
- # symbol. After loading the posts, find will collect the +author_id+ from each one and load
- # all the referenced authors with one query. Doing so will cut down the number of queries
+ # This references the name of the +belongs_to+ association that also used the <tt>:author</tt>
+ # symbol. After loading the posts, find will collect the +author_id+ from each one and load
+ # all the referenced authors with one query. Doing so will cut down the number of queries
# from 201 to 102.
#
# We can improve upon the situation further by referencing both associations in the finder with:
#
# for post in Post.find(:all, :include => [ :author, :comments ])
#
- # This will load all comments with a single query. This reduces the total number of queries
- # to 3. More generally the number of queries will be 1 plus the number of associations
+ # This will load all comments with a single query. This reduces the total number of queries
+ # to 3. More generally the number of queries will be 1 plus the number of associations
# named (except if some of the associations are polymorphic +belongs_to+ - see below).
#
# To include a deep hierarchy of associations, use a hash:
#
# for post in Post.find(:all, :include => [ :author, { :comments => { :author => :gravatar } } ])
#
- # That'll grab not only all the comments but all their authors and gravatar pictures.
- # You can mix and match symbols, arrays and hashes in any combination to describe the
+ # That'll grab not only all the comments but all their authors and gravatar pictures.
+ # You can mix and match symbols, arrays and hashes in any combination to describe the
# associations you want to load.
#
- # All of this power shouldn't fool you into thinking that you can pull out huge amounts
- # of data with no performance penalty just because you've reduced the number of queries.
- # The database still needs to send all the data to Active Record and it still needs to
- # be processed. So it's no catch-all for performance problems, but it's a great way to
+ # All of this power shouldn't fool you into thinking that you can pull out huge amounts
+ # of data with no performance penalty just because you've reduced the number of queries.
+ # The database still needs to send all the data to Active Record and it still needs to
+ # be processed. So it's no catch-all for performance problems, but it's a great way to
# cut down on the number of queries in a situation as the one described above.
#
- # Since only one table is loaded at a time, conditions or orders cannot reference tables
- # other than the main one. If this is the case Active Record falls back to the previously
+ # Since only one table is loaded at a time, conditions or orders cannot reference tables
+ # other than the main one. If this is the case Active Record falls back to the previously
# used LEFT OUTER JOIN based strategy. For example
#
- # Post.find(:all, :include => [ :author, :comments ], :conditions => ['comments.approved = ?', true])
+ # Post.includes([:author, :comments]).where(['comments.approved = ?', true]).all
#
- # This will result in a single SQL query with joins along the lines of:
+ # This will result in a single SQL query with joins along the lines of:
# <tt>LEFT OUTER JOIN comments ON comments.post_id = posts.id</tt> and
- # <tt>LEFT OUTER JOIN authors ON authors.id = posts.author_id</tt>. Note that using conditions
+ # <tt>LEFT OUTER JOIN authors ON authors.id = posts.author_id</tt>. Note that using conditions
# like this can have unintended consequences.
- # In the above example posts with no approved comments are not returned at all, because
- # the conditions apply to the SQL statement as a whole and not just to the association.
+ # In the above example posts with no approved comments are not returned at all, because
+ # the conditions apply to the SQL statement as a whole and not just to the association.
# You must disambiguate column references for this fallback to happen, for example
# <tt>:order => "author.name DESC"</tt> will work but <tt>:order => "name DESC"</tt> will not.
#
- # If you do want eager load only some members of an association it is usually more natural
+ # If you do want eager load only some members of an association it is usually more natural
# to <tt>:include</tt> an association which has conditions defined on it:
#
# class Post < ActiveRecord::Base
@@ -621,10 +727,10 @@ module ActiveRecord
#
# Post.find(:all, :include => :approved_comments)
#
- # This will load posts and eager load the +approved_comments+ association, which contains
+ # This will load posts and eager load the +approved_comments+ association, which contains
# only those comments that have been approved.
#
- # If you eager load an association with a specified <tt>:limit</tt> option, it will be ignored,
+ # If you eager load an association with a specified <tt>:limit</tt> option, it will be ignored,
# returning all the associated objects:
#
# class Picture < ActiveRecord::Base
@@ -633,7 +739,7 @@ module ActiveRecord
#
# Picture.find(:first, :include => :most_recent_comments).most_recent_comments # => returns all associated comments.
#
- # When eager loaded, conditions are interpolated in the context of the model class, not
+ # When eager loaded, conditions are interpolated in the context of the model class, not
# the model instance. Conditions are lazily interpolated before the actual model exists.
#
# Eager loading is supported with polymorphic associations.
@@ -646,20 +752,20 @@ module ActiveRecord
#
# Address.find(:all, :include => :addressable)
#
- # This will execute one query to load the addresses and load the addressables with one
+ # This will execute one query to load the addresses and load the addressables with one
# query per addressable type.
- # For example if all the addressables are either of class Person or Company then a total
- # of 3 queries will be executed. The list of addressable types to load is determined on
+ # For example if all the addressables are either of class Person or Company then a total
+ # of 3 queries will be executed. The list of addressable types to load is determined on
# the back of the addresses loaded. This is not supported if Active Record has to fallback
- # to the previous implementation of eager loading and will raise ActiveRecord::EagerLoadPolymorphicError.
- # The reason is that the parent model's type is a column value so its corresponding table
+ # to the previous implementation of eager loading and will raise ActiveRecord::EagerLoadPolymorphicError.
+ # The reason is that the parent model's type is a column value so its corresponding table
# name cannot be put in the +FROM+/+JOIN+ clauses of that query.
#
# == Table Aliasing
#
- # Active Record uses table aliasing in the case that a table is referenced multiple times
- # in a join. If a table is referenced only once, the standard table name is used. The
- # second time, the table is aliased as <tt>#{reflection_name}_#{parent_table_name}</tt>.
+ # Active Record uses table aliasing in the case that a table is referenced multiple times
+ # in a join. If a table is referenced only once, the standard table name is used. The
+ # second time, the table is aliased as <tt>#{reflection_name}_#{parent_table_name}</tt>.
# Indexes are appended for any more successive uses of the table name.
#
# Post.find :all, :joins => :comments
@@ -693,7 +799,7 @@ module ActiveRecord
# INNER JOIN categories_posts posts_categories_join INNER JOIN posts posts_categories
# INNER JOIN categories_posts categories_posts_join INNER JOIN categories categories_posts_2
#
- # If you wish to specify your own custom joins using a <tt>:joins</tt> option, those table
+ # If you wish to specify your own custom joins using a <tt>:joins</tt> option, those table
# names will take precedence over the eager associations:
#
# Post.find :all, :joins => :comments, :joins => "inner join comments ..."
@@ -703,7 +809,7 @@ module ActiveRecord
# INNER JOIN comments special_comments_posts ...
# INNER JOIN comments ...
#
- # Table aliases are automatically truncated according to the maximum length of table identifiers
+ # Table aliases are automatically truncated according to the maximum length of table identifiers
# according to the specific database.
#
# == Modules
@@ -720,9 +826,9 @@ module ActiveRecord
# end
# end
#
- # When <tt>Firm#clients</tt> is called, it will in turn call
+ # When <tt>Firm#clients</tt> is called, it will in turn call
# <tt>MyApplication::Business::Client.find_all_by_firm_id(firm.id)</tt>.
- # If you want to associate with a class in another module scope, this can be done by
+ # If you want to associate with a class in another module scope, this can be done by
# specifying the complete class name.
#
# module MyApplication
@@ -739,7 +845,7 @@ module ActiveRecord
#
# == Bi-directional associations
#
- # When you specify an association there is usually an association on the associated model
+ # When you specify an association there is usually an association on the associated model
# that specifies the same relationship in reverse. For example, with the following models:
#
# class Dungeon < ActiveRecord::Base
@@ -755,10 +861,10 @@ module ActiveRecord
# belongs_to :dungeon
# end
#
- # The +traps+ association on +Dungeon+ and the the +dungeon+ association on +Trap+ are
- # the inverse of each other and the inverse of the +dungeon+ association on +EvilWizard+
+ # The +traps+ association on +Dungeon+ and the +dungeon+ association on +Trap+ are
+ # the inverse of each other and the inverse of the +dungeon+ association on +EvilWizard+
# is the +evil_wizard+ association on +Dungeon+ (and vice-versa). By default,
- # Active Record doesn't know anything about these inverse relationships and so no object
+ # Active Record doesn't know anything about these inverse relationships and so no object
# loading optimisation is possible. For example:
#
# d = Dungeon.first
@@ -767,10 +873,10 @@ module ActiveRecord
# d.level = 10
# d.level == t.dungeon.level # => false
#
- # The +Dungeon+ instances +d+ and <tt>t.dungeon</tt> in the above example refer to
- # the same object data from the database, but are actually different in-memory copies
+ # The +Dungeon+ instances +d+ and <tt>t.dungeon</tt> in the above example refer to
+ # the same object data from the database, but are actually different in-memory copies
# of that data. Specifying the <tt>:inverse_of</tt> option on associations lets you tell
- # Active Record about inverse relationships and it will optimise object loading. For
+ # Active Record about inverse relationships and it will optimise object loading. For
# example, if we changed our model definitions to:
#
# class Dungeon < ActiveRecord::Base
@@ -786,7 +892,7 @@ module ActiveRecord
# belongs_to :dungeon, :inverse_of => :evil_wizard
# end
#
- # Then, from our code snippet above, +d+ and <tt>t.dungeon</tt> are actually the same
+ # Then, from our code snippet above, +d+ and <tt>t.dungeon</tt> are actually the same
# in-memory instance and our final <tt>d.level == t.dungeon.level</tt> will return +true+.
#
# There are limitations to <tt>:inverse_of</tt> support:
@@ -795,14 +901,81 @@ module ActiveRecord
# * does not work with <tt>:polymorphic</tt> associations.
# * for +belongs_to+ associations +has_many+ inverse associations are ignored.
#
+ # == Deleting from associations
+ #
+ # === Dependent associations
+ #
+ # +has_many+, +has_one+ and +belongs_to+ associations support the <tt>:dependent</tt> option.
+ # This allows you to specify that associated records should be deleted when the owner is
+ # deleted.
+ #
+ # For example:
+ #
+ # class Author
+ # has_many :posts, :dependent => :destroy
+ # end
+ # Author.find(1).destroy # => Will destroy all of the author's posts, too
+ #
+ # The <tt>:dependent</tt> option can have different values which specify how the deletion
+ # is done. For more information, see the documentation for this option on the different
+ # specific association types.
+ #
+ # === Delete or destroy?
+ #
+ # +has_many+ and +has_and_belongs_to_many+ associations have the methods <tt>destroy</tt>,
+ # <tt>delete</tt>, <tt>destroy_all</tt> and <tt>delete_all</tt>.
+ #
+ # For +has_and_belongs_to_many+, <tt>delete</tt> and <tt>destroy</tt> are the same: they
+ # cause the records in the join table to be removed.
+ #
+ # For +has_many+, <tt>destroy</tt> will always call the <tt>destroy</tt> method of the
+ # record(s) being removed so that callbacks are run. However <tt>delete</tt> will either
+ # do the deletion according to the strategy specified by the <tt>:dependent</tt> option, or
+ # if no <tt>:dependent</tt> option is given, then it will follow the default strategy.
+ # The default strategy is <tt>:nullify</tt> (set the foreign keys to <tt>nil</tt>), except for
+ # +has_many+ <tt>:through</tt>, where the default strategy is <tt>delete_all</tt> (delete
+ # the join records, without running their callbacks).
+ #
+ # There is also a <tt>clear</tt> method which is the same as <tt>delete_all</tt>, except that
+ # it returns the association rather than the records which have been deleted.
+ #
+ # === What gets deleted?
+ #
+ # There is a potential pitfall here: +has_and_belongs_to_many+ and +has_many+ <tt>:through</tt>
+ # associations have records in join tables, as well as the associated records. So when we
+ # call one of these deletion methods, what exactly should be deleted?
+ #
+ # The answer is that it is assumed that deletion on an association is about removing the
+ # <i>link</i> between the owner and the associated object(s), rather than necessarily the
+ # associated objects themselves. So with +has_and_belongs_to_many+ and +has_many+
+ # <tt>:through</tt>, the join records will be deleted, but the associated records won't.
+ #
+ # This makes sense if you think about it: if you were to call <tt>post.tags.delete(Tag.find_by_name('food'))</tt>
+ # you would want the 'food' tag to be unlinked from the post, rather than for the tag itself
+ # to be removed from the database.
+ #
+ # However, there are examples where this strategy doesn't make sense. For example, suppose
+ # a person has many projects, and each project has many tasks. If we deleted one of a person's
+ # tasks, we would probably not want the project to be deleted. In this scenario, the delete method
+ # won't actually work: it can only be used if the association on the join model is a
+ # +belongs_to+. In other situations you are expected to perform operations directly on
+ # either the associated records or the <tt>:through</tt> association.
+ #
+ # With a regular +has_many+ there is no distinction between the "associated records"
+ # and the "link", so there is only one choice for what gets deleted.
+ #
+ # With +has_and_belongs_to_many+ and +has_many+ <tt>:through</tt>, if you want to delete the
+ # associated records themselves, you can always do something along the lines of
+ # <tt>person.tasks.each(&:destroy)</tt>.
+ #
# == Type safety with <tt>ActiveRecord::AssociationTypeMismatch</tt>
#
- # If you attempt to assign an object to an association that doesn't match the inferred
+ # If you attempt to assign an object to an association that doesn't match the inferred
# or specified <tt>:class_name</tt>, you'll get an <tt>ActiveRecord::AssociationTypeMismatch</tt>.
#
# == Options
#
- # All of the association macros can be specialized through options. This makes cases
+ # All of the association macros can be specialized through options. This makes cases
# more complex than the simple and guessable ones possible.
module ClassMethods
# Specifies a one-to-many association. The following methods for retrieval and query of
@@ -813,12 +986,16 @@ module ActiveRecord
# An empty array is returned if none are found.
# [collection<<(object, ...)]
# Adds one or more objects to the collection by setting their foreign keys to the collection's primary key.
- # Note that this operation instantly fires update sql without waiting for the save or update call on the
+ # Note that this operation instantly fires update sql without waiting for the save or update call on the
# parent object.
# [collection.delete(object, ...)]
# Removes one or more objects from the collection by setting their foreign keys to +NULL+.
# Objects will be in addition destroyed if they're associated with <tt>:dependent => :destroy</tt>,
# and deleted if they're associated with <tt>:dependent => :delete_all</tt>.
+ #
+ # If the <tt>:through</tt> option is used, then the join records are deleted (rather than
+ # nullified) by default, but you can specify <tt>:dependent => :destroy</tt> or
+ # <tt>:dependent => :nullify</tt> to override this.
# [collection=objects]
# Replaces the collections content by deleting and adding objects as appropriate. If the <tt>:through</tt>
# option is true callbacks in the join models are triggered except destroy callbacks, since deletion is
@@ -874,24 +1051,24 @@ module ActiveRecord
# * <tt>Firm#clients.create</tt> (similar to <tt>c = Client.new("firm_id" => id); c.save; c</tt>)
# The declaration can also include an options hash to specialize the behavior of the association.
#
- # === Supported options
+ # === Options
# [:class_name]
# Specify the class name of the association. Use it only if that name can't be inferred
- # from the association name. So <tt>has_many :products</tt> will by default be linked
- # to the Product class, but if the real class name is SpecialProduct, you'll have to
+ # from the association name. So <tt>has_many :products</tt> will by default be linked
+ # to the Product class, but if the real class name is SpecialProduct, you'll have to
# specify it with this option.
# [:conditions]
# Specify the conditions that the associated objects must meet in order to be included as a +WHERE+
- # SQL fragment, such as <tt>price > 5 AND name LIKE 'B%'</tt>. Record creations from
- # the association are scoped if a hash is used.
- # <tt>has_many :posts, :conditions => {:published => true}</tt> will create published
+ # SQL fragment, such as <tt>price > 5 AND name LIKE 'B%'</tt>. Record creations from
+ # the association are scoped if a hash is used.
+ # <tt>has_many :posts, :conditions => {:published => true}</tt> will create published
# posts with <tt>@blog.posts.create</tt> or <tt>@blog.posts.build</tt>.
# [:order]
# Specify the order in which the associated objects are returned as an <tt>ORDER BY</tt> SQL fragment,
# such as <tt>last_name, first_name DESC</tt>.
# [: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_many+
+ # of this class in lower-case and "_id" suffixed. So a Person class that makes a +has_many+
# association will use "person_id" as the default <tt>:foreign_key</tt>.
# [:primary_key]
# Specify the method that returns the primary key used for the association. By default this is +id+.
@@ -902,15 +1079,17 @@ module ActiveRecord
# objects' foreign keys are set to +NULL+ *without* calling their +save+ callbacks. If set to
# <tt>:restrict</tt> this object cannot be deleted if it has any associated object.
#
- # *Warning:* This option is ignored when used with <tt>:through</tt> option.
+ # If using with the <tt>:through</tt> option, the association on the join model must be
+ # a +belongs_to+, and the records which get deleted are the join records, rather than
+ # the associated records.
#
# [:finder_sql]
# Specify a complete SQL statement to fetch the association. This is a good way to go for complex
- # associations that depend on multiple tables. Note: When this option is used, +find_in_collection+
+ # associations that depend on multiple tables. Note: When this option is used, +find_in_collection+
# is _not_ added.
# [:counter_sql]
# Specify a complete SQL statement to fetch the size of the association. If <tt>:finder_sql</tt> is
- # specified but not <tt>:counter_sql</tt>, <tt>:counter_sql</tt> will be generated by
+ # specified but not <tt>:counter_sql</tt>, <tt>:counter_sql</tt> will be generated by
# replacing <tt>SELECT ... FROM</tt> with <tt>SELECT COUNT(*) FROM</tt>.
# [:extend]
# Specify a named module for extending the proxy. See "Association extensions".
@@ -919,30 +1098,38 @@ module ActiveRecord
# [:group]
# An attribute name by which the result should be grouped. Uses the <tt>GROUP BY</tt> SQL-clause.
# [:having]
- # Combined with +:group+ this can be used to filter the records that a <tt>GROUP BY</tt>
+ # Combined with +:group+ this can be used to filter the records that a <tt>GROUP BY</tt>
# returns. Uses the <tt>HAVING</tt> SQL-clause.
# [:limit]
# An integer determining the limit on the number of rows that should be returned.
# [:offset]
- # An integer determining the offset from where the rows should be fetched. So at 5,
+ # An integer determining the offset from where the rows should be fetched. So at 5,
# it would skip the first 4 rows.
# [:select]
- # By default, this is <tt>*</tt> as in <tt>SELECT * FROM</tt>, but can be changed if
- # you, for example, want to do a join but not include the joined columns. Do not forget
+ # By default, this is <tt>*</tt> as in <tt>SELECT * FROM</tt>, but can be changed if
+ # you, for example, want to do a join but not include the joined columns. Do not forget
# to include the primary and foreign keys, otherwise it will raise an error.
# [:as]
# Specifies a polymorphic interface (See <tt>belongs_to</tt>).
# [:through]
- # Specifies a join model through which to perform the query. Options for <tt>:class_name</tt>
- # and <tt>:foreign_key</tt> are ignored, as the association uses the source reflection. You
- # can only use a <tt>:through</tt> query through a <tt>belongs_to</tt>, <tt>has_one</tt>
- # or <tt>has_many</tt> association on the join model. The collection of join models
- # can be managed via the collection API. For example, new join models are created for
- # newly associated objects, and if some are gone their rows are deleted (directly,
- # no destroy callbacks are triggered).
+ # Specifies an association through which to perform the query. This can be any other type
+ # of association, including other <tt>:through</tt> associations. Options for <tt>:class_name</tt>,
+ # <tt>:primary_key</tt> and <tt>:foreign_key</tt> are ignored, as the association uses the
+ # source reflection.
+ #
+ # If the association on the join model is a +belongs_to+, the collection can be modified
+ # and the records on the <tt>:through</tt> model will be automatically created and removed
+ # as appropriate. Otherwise, the collection is read-only, so you should manipulate the
+ # <tt>:through</tt> association directly.
+ #
+ # If you are going to modify the association (rather than just read from it), then it is
+ # a good idea to set the <tt>:inverse_of</tt> option on the source association on the
+ # join model. This allows associated records to be built which will automatically create
+ # the appropriate join model records when they are saved. (See the 'Association Join Models'
+ # section above.)
# [:source]
- # Specifies the source association name used by <tt>has_many :through</tt> queries.
- # Only use it if the name cannot be inferred from the association.
+ # Specifies the source association name used by <tt>has_many :through</tt> queries.
+ # Only use it if the name cannot be inferred from the association.
# <tt>has_many :subscribers, :through => :subscriptions</tt> will look for either <tt>:subscribers</tt> or
# <tt>:subscriber</tt> on Subscription, unless a <tt>:source</tt> is given.
# [:source_type]
@@ -953,15 +1140,14 @@ module ActiveRecord
# [:readonly]
# 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.
+ # If +false+, don't validate the associated objects when saving the parent object. true by default.
# [:autosave]
- # If true, always save the associated objects or destroy them if marked for destruction,
- # when saving the parent object.
- # If false, never save or destroy the associated objects.
+ # If true, always save the associated objects or destroy them if marked for destruction,
+ # when saving the parent object. If false, never save or destroy the associated objects.
# By default, only save associated objects that are new records.
# [:inverse_of]
- # Specifies the name of the <tt>belongs_to</tt> association on the associated object
- # that is the inverse of this <tt>has_many</tt> association. Does not work in combination
+ # Specifies the name of the <tt>belongs_to</tt> association on the associated object
+ # that is the inverse of this <tt>has_many</tt> association. Does not work in combination
# with <tt>:through</tt> or <tt>:as</tt> options.
# See ActiveRecord::Associations::ClassMethods's overview on Bi-directional associations for more detail.
#
@@ -979,16 +1165,8 @@ module ActiveRecord
# 'FROM people p, post_subscriptions ps ' +
# 'WHERE ps.post_id = #{id} AND ps.person_id = p.id ' +
# '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_association_callbacks(reflection.name, reflection.options)
-
- if options[:through]
- collection_accessor_methods(reflection, HasManyThroughAssociation)
- else
- collection_accessor_methods(reflection, HasManyAssociation)
- end
+ def has_many(name, options = {}, &extension)
+ Builder::HasMany.build(self, name, options, &extension)
end
# Specifies a one-to-one association with another class. This method should only be used
@@ -1006,12 +1184,14 @@ module ActiveRecord
# [build_association(attributes = {})]
# Returns a new object of the associated type that has been instantiated
# with +attributes+ and linked to this object through a foreign key, but has not
- # yet been saved. <b>Note:</b> This ONLY works if an association already exists.
- # It will NOT work if the association is +nil+.
+ # yet been saved.
# [create_association(attributes = {})]
# Returns a new object of the associated type that has been instantiated
# with +attributes+, linked to this object through a foreign key, and that
# has already been saved (if it passed the validation).
+ # [create_association!(attributes = {})]
+ # Does the same as <tt>create_association</tt>, but raises <tt>ActiveRecord::RecordInvalid</tt>
+ # if the record is invalid.
#
# (+association+ is replaced with the symbol passed as the first argument, so
# <tt>has_one :manager</tt> would add among others <tt>manager.nil?</tt>.)
@@ -1023,6 +1203,7 @@ module ActiveRecord
# * <tt>Account#beneficiary=(beneficiary)</tt> (similar to <tt>beneficiary.account_id = account.id; beneficiary.save</tt>)
# * <tt>Account#build_beneficiary</tt> (similar to <tt>Beneficiary.new("account_id" => id)</tt>)
# * <tt>Account#create_beneficiary</tt> (similar to <tt>b = Beneficiary.new("account_id" => id); b.save; b</tt>)
+ # * <tt>Account#create_beneficiary!</tt> (similar to <tt>b = Beneficiary.new("account_id" => id); b.save!; b</tt>)
#
# === Options
#
@@ -1036,19 +1217,19 @@ module ActiveRecord
# [:conditions]
# Specify the conditions that the associated object must meet in order to be included as a +WHERE+
# SQL fragment, such as <tt>rank = 5</tt>. Record creation from the association is scoped if a hash
- # is used. <tt>has_one :account, :conditions => {:enabled => true}</tt> will create
+ # is used. <tt>has_one :account, :conditions => {:enabled => true}</tt> will create
# an enabled account with <tt>@company.create_account</tt> or <tt>@company.build_account</tt>.
# [:order]
# Specify the order in which the associated objects are returned as an <tt>ORDER BY</tt> SQL fragment,
# such as <tt>last_name, first_name DESC</tt>.
# [:dependent]
# If set to <tt>:destroy</tt>, the associated object is destroyed when this object is. If set to
- # <tt>:delete</tt>, the associated object is deleted *without* calling its destroy method.
- # If set to <tt>:nullify</tt>, the associated object's foreign key is set to +NULL+.
+ # <tt>:delete</tt>, the associated object is deleted *without* calling its destroy method.
+ # If set to <tt>:nullify</tt>, the associated object's foreign key is set to +NULL+.
# Also, association is assigned.
# [: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_one+ association
+ # of this class in lower-case and "_id" suffixed. So a Person class that makes a +has_one+ association
# will use "person_id" as the default <tt>:foreign_key</tt>.
# [:primary_key]
# Specify the method that returns the primary key used for the association. By default this is +id+.
@@ -1057,17 +1238,17 @@ module ActiveRecord
# [:as]
# Specifies a polymorphic interface (See <tt>belongs_to</tt>).
# [:select]
- # By default, this is <tt>*</tt> as in <tt>SELECT * FROM</tt>, but can be changed if, for example,
- # you want to do a join but not include the joined columns. Do not forget to include the
+ # By default, this is <tt>*</tt> as in <tt>SELECT * FROM</tt>, but can be changed if, for example,
+ # you want to do a join but not include the joined columns. Do not forget to include the
# primary and foreign keys, otherwise it will raise an error.
# [:through]
- # Specifies a Join Model through which to perform the query. Options for <tt>:class_name</tt>
- # and <tt>:foreign_key</tt> are ignored, as the association uses the source reflection. You
- # can only use a <tt>:through</tt> query through a <tt>has_one</tt> or <tt>belongs_to</tt>
- # association on the join model.
+ # Specifies a Join Model through which to perform the query. Options for <tt>:class_name</tt>,
+ # <tt>:primary_key</tt>, and <tt>:foreign_key</tt> are ignored, as the association uses the
+ # source reflection. You can only use a <tt>:through</tt> query through a <tt>has_one</tt>
+ # or <tt>belongs_to</tt> association on the join model.
# [: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.
+ # 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.
# [:source_type]
@@ -1076,20 +1257,20 @@ module ActiveRecord
# [: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.
+ # 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,
+ # If true, always save the associated object or destroy it if marked for destruction,
# when saving the parent object. If false, never save or destroy the associated object.
# By default, only save the associated object if it's a new record.
# [:inverse_of]
- # Specifies the name of the <tt>belongs_to</tt> association on the associated object
- # that is the inverse of this <tt>has_one</tt> association. Does not work in combination
+ # Specifies the name of the <tt>belongs_to</tt> association on the associated object
+ # that is the inverse of this <tt>has_one</tt> association. Does not work in combination
# with <tt>:through</tt> or <tt>:as</tt> options.
# See ActiveRecord::Associations::ClassMethods's overview on Bi-directional associations for more detail.
#
# Option examples:
# has_one :credit_card, :dependent => :destroy # destroys the associated credit card
- # has_one :credit_card, :dependent => :nullify # updates the associated records foreign
+ # has_one :credit_card, :dependent => :nullify # updates the associated records foreign
# # key value to NULL rather than destroying it
# has_one :last_comment, :class_name => "Comment", :order => "posted_on"
# has_one :project_manager, :class_name => "Person", :conditions => "role = 'project_manager'"
@@ -1097,17 +1278,8 @@ module ActiveRecord
# has_one :boss, :readonly => :true
# has_one :club, :through => :membership
# has_one :primary_address, :through => :addressables, :conditions => ["addressable.primary = ?", true], :source => :addressable
- def has_one(association_id, options = {})
- if options[:through]
- reflection = create_has_one_through_reflection(association_id, options)
- association_accessor_methods(reflection, ActiveRecord::Associations::HasOneThroughAssociation)
- else
- reflection = create_has_one_reflection(association_id, options)
- association_accessor_methods(reflection, HasOneAssociation)
- association_constructor_method(:build, reflection, HasOneAssociation)
- association_constructor_method(:create, reflection, HasOneAssociation)
- configure_dependency_for_has_one(reflection)
- end
+ def has_one(name, options = {})
+ Builder::HasOne.build(self, name, options)
end
# Specifies a one-to-one association with another class. This method should only be used
@@ -1129,6 +1301,9 @@ module ActiveRecord
# Returns a new object of the associated type that has been instantiated
# with +attributes+, linked to this object through a foreign key, and that
# has already been saved (if it passed the validation).
+ # [create_association!(attributes = {})]
+ # Does the same as <tt>create_association</tt>, but raises <tt>ActiveRecord::RecordInvalid</tt>
+ # if the record is invalid.
#
# (+association+ is replaced with the symbol passed as the first argument, so
# <tt>belongs_to :author</tt> would add among others <tt>author.nil?</tt>.)
@@ -1140,6 +1315,7 @@ module ActiveRecord
# * <tt>Post#author=(author)</tt> (similar to <tt>post.author_id = author.id</tt>)
# * <tt>Post#build_author</tt> (similar to <tt>post.author = Author.new</tt>)
# * <tt>Post#create_author</tt> (similar to <tt>post.author = Author.new; post.author.save; post.author</tt>)
+ # * <tt>Post#create_author!</tt> (similar to <tt>post.author = Author.new; post.author.save!; post.author</tt>)
# The declaration can also include an options hash to specialize the behavior of the association.
#
# === Options
@@ -1152,33 +1328,38 @@ module ActiveRecord
# 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>.
# [:select]
- # By default, this is <tt>*</tt> as in <tt>SELECT * FROM</tt>, but can be changed
- # if, for example, you want to do a join but not include the joined columns. Do not
+ # By default, this is <tt>*</tt> as in <tt>SELECT * FROM</tt>, but can be changed
+ # if, for example, you want to do a join but not include the joined columns. Do not
# forget to include the primary and foreign keys, otherwise it will raise an error.
# [:foreign_key]
# Specify the foreign key used for the association. By default this is guessed to be the name
- # of the association with an "_id" suffix. So a class that defines a <tt>belongs_to :person</tt>
- # association will use "person_id" as the default <tt>:foreign_key</tt>. Similarly,
- # <tt>belongs_to :favorite_person, :class_name => "Person"</tt> will use a foreign key
+ # of the association with an "_id" suffix. So a class that defines a <tt>belongs_to :person</tt>
+ # association will use "person_id" as the default <tt>:foreign_key</tt>. Similarly,
+ # <tt>belongs_to :favorite_person, :class_name => "Person"</tt> will use a foreign key
# of "favorite_person_id".
+ # [:foreign_type]
+ # Specify the column used to store the associated object's type, if this is a polymorphic
+ # association. By default this is guessed to be the name of the association with a "_type"
+ # suffix. So a class that defines a <tt>belongs_to :taggable, :polymorphic => true</tt>
+ # association will use "taggable_type" as the default <tt>:foreign_type</tt>.
# [:primary_key]
- # Specify the method that returns the primary key of associated object used for the association.
+ # Specify the method that returns the primary key of associated object used for the association.
# By default this is id.
# [:dependent]
# If set to <tt>:destroy</tt>, the associated object is destroyed when this object is. If set to
- # <tt>:delete</tt>, the associated object is deleted *without* calling its destroy method.
- # This option should not be specified when <tt>belongs_to</tt> is used in conjunction with
+ # <tt>:delete</tt>, the associated object is deleted *without* calling its destroy method.
+ # This option should not be specified when <tt>belongs_to</tt> is used in conjunction with
# a <tt>has_many</tt> relationship on another class because of the potential to leave
# orphaned records behind.
# [:counter_cache]
# Caches the number of belonging objects on the associate class through the use of +increment_counter+
- # and +decrement_counter+. The counter cache is incremented when an object of this
- # class is created and decremented when it's destroyed. This requires that a column
+ # and +decrement_counter+. The counter cache is incremented when an object of this
+ # class is created and decremented when it's destroyed. This requires that a column
# named <tt>#{table_name}_count</tt> (such as +comments_count+ for a belonging Comment class)
- # is used on the associate class (such as a Post class). You can also specify a custom counter
- # cache column by providing a column name instead of a +true+/+false+ value to this
+ # is used on the associate class (such as a Post class). You can also specify a custom counter
+ # cache column by providing a column name instead of a +true+/+false+ value to this
# option (e.g., <tt>:counter_cache => :my_custom_counter</tt>.)
- # Note: Specifying a counter cache will add it to that model's list of readonly attributes
+ # Note: Specifying a counter cache will add it to that model's list of readonly attributes
# using +attr_readonly+.
# [:include]
# Specify second-order associations that should be eager loaded when this object is loaded.
@@ -1189,19 +1370,19 @@ module ActiveRecord
# [:readonly]
# 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.
+ # 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
+ # If true, always save the associated object or destroy it if marked for destruction, when
# saving the parent object.
# If false, never save or destroy the associated object.
# By default, only save the associated object if it's a new record.
# [:touch]
- # If true, the associated object will be touched (the updated_at/on attributes set to now)
- # when this record is either saved or destroyed. If you specify a symbol, that attribute
+ # If true, the associated object will be touched (the updated_at/on attributes set to now)
+ # when this record is either saved or destroyed. If you specify a symbol, that attribute
# will be updated with the current time instead of the updated_at/on attribute.
# [:inverse_of]
- # Specifies the name of the <tt>has_one</tt> or <tt>has_many</tt> association on the associated
- # object that is the inverse of this <tt>belongs_to</tt> association. Does not work in
+ # Specifies the name of the <tt>has_one</tt> or <tt>has_many</tt> association on the associated
+ # object that is the inverse of this <tt>belongs_to</tt> association. Does not work in
# combination with the <tt>:polymorphic</tt> options.
# See ActiveRecord::Associations::ClassMethods's overview on Bi-directional associations for more detail.
#
@@ -1216,29 +1397,16 @@ module ActiveRecord
# belongs_to :post, :counter_cache => true
# belongs_to :company, :touch => true
# belongs_to :company, :touch => :employees_last_updated_at
- def belongs_to(association_id, options = {})
- reflection = create_belongs_to_reflection(association_id, options)
-
- if reflection.options[:polymorphic]
- association_accessor_methods(reflection, BelongsToPolymorphicAssociation)
- else
- association_accessor_methods(reflection, BelongsToAssociation)
- association_constructor_method(:build, reflection, BelongsToAssociation)
- association_constructor_method(:create, reflection, BelongsToAssociation)
- end
-
- add_counter_cache_callbacks(reflection) if options[:counter_cache]
- add_touch_callbacks(reflection, options[:touch]) if options[:touch]
-
- configure_dependency_for_belongs_to(reflection)
+ def belongs_to(name, options = {})
+ Builder::BelongsTo.build(self, name, options)
end
# Specifies a many-to-many relationship with another class. This associates two classes via an
# intermediate join table. Unless the join table is explicitly specified as an option, it is
# guessed using the lexical order of the class names. So a join between Developer and Project
- # will give the default join table name of "developers_projects" because "D" outranks "P".
- # Note that this precedence is calculated using the <tt><</tt> operator for String. This
- # means that if the strings are of different lengths, and the strings are equal when compared
+ # will give the default join table name of "developers_projects" because "D" outranks "P".
+ # Note that this precedence is calculated using the <tt><</tt> operator for String. This
+ # means that if the strings are of different lengths, and the strings are equal when compared
# up to the shortest length, then the longer string is considered of higher
# lexical precedence than the shorter one. For example, one would expect the tables "paper_boxes" and "papers"
# to generate a join table name of "papers_paper_boxes" because of the length of the name "paper_boxes",
@@ -1261,12 +1429,6 @@ module ActiveRecord
# end
# end
#
- # Deprecated: Any additional fields added to the join table will be placed as attributes when
- # pulling records out through +has_and_belongs_to_many+ associations. Records returned from join
- # tables with additional attributes will be marked as readonly (because we can't save changes
- # to the additional attributes). It's strongly recommended that you upgrade any
- # associations with attributes to a real join model (see introduction).
- #
# Adds the following methods for retrieval and query:
#
# [collection(force_reload = false)]
@@ -1275,7 +1437,7 @@ module ActiveRecord
# [collection<<(object, ...)]
# Adds one or more objects to the collection by creating associations in the join table
# (<tt>collection.push</tt> and <tt>collection.concat</tt> are aliases to this method).
- # Note that this operation instantly fires update sql without waiting for the save or update call on the
+ # Note that this operation instantly fires update sql without waiting for the save or update call on the
# parent object.
# [collection.delete(object, ...)]
# Removes one or more objects from the collection by removing their associations from the join table.
@@ -1304,7 +1466,7 @@ module ActiveRecord
# with +attributes+ and linked to this object through the join table, but has not yet been saved.
# [collection.create(attributes = {})]
# Returns a new object of the collection type that has been instantiated
- # with +attributes+, linked to this object through the join table, and that has already been
+ # with +attributes+, linked to this object through the join table, and that has already been
# saved (if it passed the validation).
#
# (+collection+ is replaced with the symbol passed as the first argument, so
@@ -1340,8 +1502,8 @@ module ActiveRecord
# MUST be declared underneath any +has_and_belongs_to_many+ declaration in order to work.
# [: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 to Project will use "person_id" as the
+ # of this class in lower-case and "_id" suffixed. So a Person class that makes
+ # a +has_and_belongs_to_many+ association to Project will use "person_id" as the
# default <tt>:foreign_key</tt>.
# [:association_foreign_key]
# Specify the foreign key used for the association on the receiving side of the association.
@@ -1350,7 +1512,7 @@ module ActiveRecord
# 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
+ # SQL fragment, such as <tt>authorized = 1</tt>. Record creations from the association are
# scoped if a hash is used.
# <tt>has_many :posts, :conditions => {:published => true}</tt> will create published posts with <tt>@blog.posts.create</tt>
# or <tt>@blog.posts.build</tt>.
@@ -1363,7 +1525,7 @@ module ActiveRecord
# Overwrite the default generated SQL statement used to fetch the association with a manual statement
# [:counter_sql]
# Specify a complete SQL statement to fetch the size of the association. If <tt>:finder_sql</tt> is
- # specified but not <tt>:counter_sql</tt>, <tt>:counter_sql</tt> will be generated by
+ # specified but not <tt>:counter_sql</tt>, <tt>:counter_sql</tt> will be generated by
# replacing <tt>SELECT ... FROM</tt> with <tt>SELECT COUNT(*) FROM</tt>.
# [:delete_sql]
# Overwrite the default generated SQL statement used to remove links between the associated
@@ -1378,23 +1540,23 @@ module ActiveRecord
# [:group]
# An attribute name by which the result should be grouped. Uses the <tt>GROUP BY</tt> SQL-clause.
# [:having]
- # Combined with +:group+ this can be used to filter the records that a <tt>GROUP BY</tt> returns.
+ # Combined with +:group+ this can be used to filter the records that a <tt>GROUP BY</tt> returns.
# Uses the <tt>HAVING</tt> SQL-clause.
# [:limit]
# An integer determining the limit on the number of rows that should be returned.
# [:offset]
- # An integer determining the offset from where the rows should be fetched. So at 5,
+ # An integer determining the offset from where the rows should be fetched. So at 5,
# it would skip the first 4 rows.
# [:select]
- # By default, this is <tt>*</tt> as in <tt>SELECT * FROM</tt>, but can be changed if, for example,
- # you want to do a join but not include the joined columns. Do not forget to include the primary
+ # By default, this is <tt>*</tt> as in <tt>SELECT * FROM</tt>, but can be changed if, for example,
+ # you want to do a join but not include the joined columns. Do not forget to include the primary
# and foreign keys, otherwise it will raise an error.
# [:readonly]
# 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.
+ # If +false+, don't validate the associated objects when saving the parent object. +true+ by default.
# [:autosave]
- # If true, always save the associated objects or destroy them if marked for destruction, when
+ # If true, always save the associated objects or destroy them if marked for destruction, when
# saving the parent object.
# If false, never save or destroy the associated objects.
# By default, only save associated objects that are new records.
@@ -1407,845 +1569,9 @@ module ActiveRecord
# has_and_belongs_to_many :categories, :readonly => true
# has_and_belongs_to_many :active_projects, :join_table => 'developers_projects', :delete_sql =>
# 'DELETE FROM developers_projects WHERE active=1 AND developer_id = #{id} AND project_id = #{record.id}'
- def has_and_belongs_to_many(association_id, options = {}, &extension)
- reflection = create_has_and_belongs_to_many_reflection(association_id, options, &extension)
- collection_accessor_methods(reflection, HasAndBelongsToManyAssociation)
-
- # Don't use a before_destroy callback since users' before_destroy
- # callbacks will be executed after the association is wiped out.
- include Module.new {
- class_eval <<-RUBY, __FILE__, __LINE__ + 1
- def destroy # def destroy
- super # super
- #{reflection.name}.clear # posts.clear
- end # end
- RUBY
- }
-
- add_association_callbacks(reflection.name, options)
+ def has_and_belongs_to_many(name, options = {}, &extension)
+ Builder::HasAndBelongsToMany.build(self, name, options, &extension)
end
-
- private
- # Generates a join table name from two provided table names.
- # The names in the join table names end up in lexicographic order.
- #
- # join_table_name("members", "clubs") # => "clubs_members"
- # join_table_name("members", "special_clubs") # => "members_special_clubs"
- def join_table_name(first_table_name, second_table_name)
- if first_table_name < second_table_name
- join_table = "#{first_table_name}_#{second_table_name}"
- else
- join_table = "#{second_table_name}_#{first_table_name}"
- end
-
- table_name_prefix + join_table + table_name_suffix
- end
-
- def association_accessor_methods(reflection, association_proxy_class)
- redefine_method(reflection.name) do |*params|
- force_reload = params.first unless params.empty?
- association = association_instance_get(reflection.name)
-
- if association.nil? || force_reload
- association = association_proxy_class.new(self, reflection)
- retval = force_reload ? reflection.klass.uncached { association.reload } : association.reload
- if retval.nil? and association_proxy_class == BelongsToAssociation
- association_instance_set(reflection.name, nil)
- return nil
- end
- association_instance_set(reflection.name, association)
- end
-
- association.target.nil? ? nil : association
- end
-
- redefine_method("loaded_#{reflection.name}?") do
- association = association_instance_get(reflection.name)
- association && association.loaded?
- end
-
- redefine_method("#{reflection.name}=") do |new_value|
- association = association_instance_get(reflection.name)
-
- if association.nil? || association.target != new_value
- association = association_proxy_class.new(self, reflection)
- end
-
- association.replace(new_value)
- association_instance_set(reflection.name, new_value.nil? ? nil : association)
- end
-
- redefine_method("set_#{reflection.name}_target") do |target|
- return if target.nil? and association_proxy_class == BelongsToAssociation
- association = association_proxy_class.new(self, reflection)
- association.target = target
- association_instance_set(reflection.name, association)
- end
- end
-
- def collection_reader_method(reflection, association_proxy_class)
- redefine_method(reflection.name) do |*params|
- force_reload = params.first unless params.empty?
- association = association_instance_get(reflection.name)
-
- unless association
- association = association_proxy_class.new(self, reflection)
- association_instance_set(reflection.name, association)
- end
-
- reflection.klass.uncached { association.reload } if force_reload
-
- association
- end
-
- redefine_method("#{reflection.name.to_s.singularize}_ids") do
- if send(reflection.name).loaded? || reflection.options[:finder_sql]
- send(reflection.name).map(&:id)
- else
- if reflection.through_reflection && reflection.source_reflection.belongs_to?
- through = reflection.through_reflection
- primary_key = reflection.source_reflection.primary_key_name
- send(through.name).select("DISTINCT #{through.quoted_table_name}.#{primary_key}").map!(&:"#{primary_key}")
- else
- send(reflection.name).select("#{reflection.quoted_table_name}.#{reflection.klass.primary_key}").except(:includes).map!(&:id)
- end
- end
- end
-
- end
-
- def collection_accessor_methods(reflection, association_proxy_class, writer = true)
- collection_reader_method(reflection, association_proxy_class)
-
- if writer
- redefine_method("#{reflection.name}=") do |new_value|
- # Loads proxy class instance (defined in collection_reader_method) if not already loaded
- association = send(reflection.name)
- association.replace(new_value)
- association
- end
-
- redefine_method("#{reflection.name.to_s.singularize}_ids=") do |new_value|
- pk_column = reflection.primary_key_column
- ids = (new_value || []).reject { |nid| nid.blank? }
- ids.map!{ |i| pk_column.type_cast(i) }
- send("#{reflection.name}=", reflection.klass.find(ids).index_by(&:id).values_at(*ids))
- end
- end
- end
-
- def association_constructor_method(constructor, reflection, association_proxy_class)
- redefine_method("#{constructor}_#{reflection.name}") do |*params|
- attributees = params.first unless params.empty?
- replace_existing = params[1].nil? ? true : params[1]
- association = association_instance_get(reflection.name)
-
- unless association
- association = association_proxy_class.new(self, reflection)
- association_instance_set(reflection.name, association)
- end
-
- if association_proxy_class == HasOneAssociation
- association.send(constructor, attributees, replace_existing)
- else
- association.send(constructor, attributees)
- end
- end
- end
-
- def add_counter_cache_callbacks(reflection)
- cache_column = reflection.counter_cache_column
-
- method_name = "belongs_to_counter_cache_after_create_for_#{reflection.name}".to_sym
- define_method(method_name) do
- association = send(reflection.name)
- association.class.increment_counter(cache_column, association.id) unless association.nil?
- end
- after_create(method_name)
-
- method_name = "belongs_to_counter_cache_before_destroy_for_#{reflection.name}".to_sym
- define_method(method_name) do
- association = send(reflection.name)
- association.class.decrement_counter(cache_column, association.id) unless association.nil?
- end
- before_destroy(method_name)
-
- module_eval(
- "#{reflection.class_name}.send(:attr_readonly,\"#{cache_column}\".intern) if defined?(#{reflection.class_name}) && #{reflection.class_name}.respond_to?(:attr_readonly)", __FILE__, __LINE__
- )
- end
-
- def add_touch_callbacks(reflection, touch_attribute)
- method_name = :"belongs_to_touch_after_save_or_destroy_for_#{reflection.name}"
- redefine_method(method_name) do
- association = send(reflection.name)
-
- if touch_attribute == true
- association.touch unless association.nil?
- else
- association.touch(touch_attribute) unless association.nil?
- end
- end
- after_save(method_name)
- after_touch(method_name)
- after_destroy(method_name)
- end
-
- # Creates before_destroy callback methods that nullify, delete or destroy
- # has_many associated objects, according to the defined :dependent rule.
- #
- # See HasManyAssociation#delete_records for more information. In general
- # - delete children if the option is set to :destroy or :delete_all
- # - set the foreign key to NULL if the option is set to :nullify
- # - do not delete the parent record if there is any child record if the
- # option is set to :restrict
- #
- # The +extra_conditions+ parameter, which is not used within the main
- # Active Record codebase, is meant to allow plugins to define extra
- # finder conditions.
- def configure_dependency_for_has_many(reflection, extra_conditions = nil)
- if reflection.options.include?(:dependent)
- case reflection.options[:dependent]
- when :destroy
- method_name = "has_many_dependent_destroy_for_#{reflection.name}".to_sym
- define_method(method_name) do
- send(reflection.name).each do |o|
- # No point in executing the counter update since we're going to destroy the parent anyway
- counter_method = ('belongs_to_counter_cache_before_destroy_for_' + self.class.name.downcase).to_sym
- if(o.respond_to? counter_method) then
- class << o
- self
- end.send(:define_method, counter_method, Proc.new {})
- end
- o.destroy
- end
- end
- before_destroy method_name
- when :delete_all
- before_destroy do |record|
- self.class.send(:delete_all_has_many_dependencies,
- record,
- reflection.name,
- reflection.klass,
- reflection.dependent_conditions(record, self.class, extra_conditions))
- end
- when :nullify
- before_destroy do |record|
- self.class.send(:nullify_has_many_dependencies,
- record,
- reflection.name,
- reflection.klass,
- reflection.primary_key_name,
- reflection.dependent_conditions(record, self.class, extra_conditions))
- end
- when :restrict
- method_name = "has_many_dependent_restrict_for_#{reflection.name}".to_sym
- define_method(method_name) do
- unless send(reflection.name).empty?
- raise DeleteRestrictionError.new(reflection)
- end
- end
- before_destroy method_name
- else
- raise ArgumentError, "The :dependent option expects either :destroy, :delete_all, :nullify or :restrict (#{reflection.options[:dependent].inspect})"
- end
- end
- end
-
- # Creates before_destroy callback methods that nullify, delete or destroy
- # has_one associated objects, according to the defined :dependent rule.
- # If the association is marked as :dependent => :restrict, create a callback
- # that prevents deleting entirely.
- def configure_dependency_for_has_one(reflection)
- if reflection.options.include?(:dependent)
- name = reflection.options[:dependent]
- method_name = :"has_one_dependent_#{name}_for_#{reflection.name}"
-
- case name
- when :destroy, :delete
- class_eval <<-eoruby, __FILE__, __LINE__ + 1
- def #{method_name}
- association = #{reflection.name}
- association.#{name} if association
- end
- eoruby
- when :nullify
- class_eval <<-eoruby, __FILE__, __LINE__ + 1
- def #{method_name}
- association = #{reflection.name}
- association.update_attribute(#{reflection.primary_key_name.inspect}, nil) if association
- end
- eoruby
- when :restrict
- method_name = "has_one_dependent_restrict_for_#{reflection.name}".to_sym
- define_method(method_name) do
- unless send(reflection.name).nil?
- raise DeleteRestrictionError.new(reflection)
- end
- end
- before_destroy method_name
- else
- raise ArgumentError, "The :dependent option expects either :destroy, :delete, :nullify or :restrict (#{reflection.options[:dependent].inspect})"
- end
-
- before_destroy method_name
- end
- end
-
- def configure_dependency_for_belongs_to(reflection)
- if reflection.options.include?(:dependent)
- name = reflection.options[:dependent]
-
- unless [:destroy, :delete].include?(name)
- raise ArgumentError, "The :dependent option expects either :destroy or :delete (#{reflection.options[:dependent].inspect})"
- end
-
- method_name = :"belongs_to_dependent_#{name}_for_#{reflection.name}"
- class_eval <<-eoruby, __FILE__, __LINE__ + 1
- def #{method_name}
- association = #{reflection.name}
- association.#{name} if association
- end
- eoruby
- after_destroy method_name
- end
- end
-
- def delete_all_has_many_dependencies(record, reflection_name, association_class, dependent_conditions)
- association_class.delete_all(dependent_conditions)
- end
-
- def nullify_has_many_dependencies(record, reflection_name, association_class, primary_key_name, dependent_conditions)
- association_class.update_all("#{primary_key_name} = NULL", dependent_conditions)
- end
-
- mattr_accessor :valid_keys_for_has_many_association
- @@valid_keys_for_has_many_association = [
- :class_name, :table_name, :foreign_key, :primary_key,
- :dependent,
- :select, :conditions, :include, :order, :group, :having, :limit, :offset,
- :as, :through, :source, :source_type,
- :uniq,
- :finder_sql, :counter_sql,
- :before_add, :after_add, :before_remove, :after_remove,
- :extend, :readonly,
- :validate, :inverse_of
- ]
-
- def create_has_many_reflection(association_id, options, &extension)
- options.assert_valid_keys(valid_keys_for_has_many_association)
- options[:extend] = create_extension_modules(association_id, extension, options[:extend])
-
- create_reflection(:has_many, association_id, options, self)
- end
-
- mattr_accessor :valid_keys_for_has_one_association
- @@valid_keys_for_has_one_association = [
- :class_name, :foreign_key, :remote, :select, :conditions, :order,
- :include, :dependent, :counter_cache, :extend, :as, :readonly,
- :validate, :primary_key, :inverse_of
- ]
-
- def create_has_one_reflection(association_id, options)
- options.assert_valid_keys(valid_keys_for_has_one_association)
- create_reflection(:has_one, association_id, options, self)
- end
-
- def create_has_one_through_reflection(association_id, options)
- options.assert_valid_keys(
- :class_name, :foreign_key, :remote, :select, :conditions, :order, :include, :dependent, :counter_cache, :extend, :as, :through, :source, :source_type, :validate
- )
- create_reflection(:has_one, association_id, options, self)
- end
-
- mattr_accessor :valid_keys_for_belongs_to_association
- @@valid_keys_for_belongs_to_association = [
- :class_name, :primary_key, :foreign_key, :foreign_type, :remote, :select, :conditions,
- :include, :dependent, :counter_cache, :extend, :polymorphic, :readonly,
- :validate, :touch, :inverse_of
- ]
-
- def create_belongs_to_reflection(association_id, options)
- options.assert_valid_keys(valid_keys_for_belongs_to_association)
- reflection = create_reflection(:belongs_to, association_id, options, self)
-
- if options[:polymorphic]
- reflection.options[:foreign_type] ||= reflection.class_name.underscore + "_type"
- end
-
- reflection
- end
-
- mattr_accessor :valid_keys_for_has_and_belongs_to_many_association
- @@valid_keys_for_has_and_belongs_to_many_association = [
- :class_name, :table_name, :join_table, :foreign_key, :association_foreign_key,
- :select, :conditions, :include, :order, :group, :having, :limit, :offset,
- :uniq,
- :finder_sql, :counter_sql, :delete_sql, :insert_sql,
- :before_add, :after_add, :before_remove, :after_remove,
- :extend, :readonly,
- :validate
- ]
-
- def create_has_and_belongs_to_many_reflection(association_id, options, &extension)
- options.assert_valid_keys(valid_keys_for_has_and_belongs_to_many_association)
- 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))
- if connection.supports_primary_key? && (connection.primary_key(reflection.options[:join_table]) rescue false)
- raise HasAndBelongsToManyAssociationWithPrimaryKeyError.new(reflection)
- end
-
- reflection
- end
-
- def add_association_callbacks(association_name, options)
- callbacks = %w(before_add after_add before_remove after_remove)
- callbacks.each do |callback_name|
- full_callback_name = "#{callback_name}_for_#{association_name}"
- defined_callbacks = options[callback_name.to_sym]
- if options.has_key?(callback_name.to_sym)
- class_inheritable_reader full_callback_name.to_sym
- write_inheritable_attribute(full_callback_name.to_sym, [defined_callbacks].flatten)
- else
- write_inheritable_attribute(full_callback_name.to_sym, [])
- end
- end
- end
-
- def create_extension_modules(association_id, block_extension, extensions)
- if block_extension
- extension_module_name = "#{self.to_s.demodulize}#{association_id.to_s.camelize}AssociationExtension"
-
- silence_warnings do
- self.parent.const_set(extension_module_name, Module.new(&block_extension))
- end
- Array.wrap(extensions).push("#{self.parent}::#{extension_module_name}".constantize)
- else
- Array.wrap(extensions)
- end
- end
-
- class JoinDependency # :nodoc:
- attr_reader :joins, :reflections, :table_aliases
-
- def initialize(base, associations, joins)
- @joins = [JoinBase.new(base, joins)]
- @associations = associations
- @reflections = []
- @base_records_hash = {}
- @base_records_in_order = []
- @table_aliases = Hash.new { |aliases, table| aliases[table] = 0 }
- @table_aliases[base.table_name] = 1
- build(associations)
- end
-
- def graft(*associations)
- associations.each do |association|
- join_associations.detect {|a| association == a} ||
- build(association.reflection.name, association.find_parent_in(self) || join_base, association.join_class)
- end
- self
- end
-
- def join_associations
- @joins[1..-1].to_a
- end
-
- def join_base
- @joins[0]
- end
-
- def count_aliases_from_table_joins(name)
- # quoted_name should be downcased as some database adapters (Oracle) return quoted name in uppercase
- quoted_name = join_base.active_record.connection.quote_table_name(name.downcase).downcase
- join_sql = join_base.table_joins.to_s.downcase
- join_sql.blank? ? 0 :
- # Table names
- join_sql.scan(/join(?:\s+\w+)?\s+#{quoted_name}\son/).size +
- # Table aliases
- join_sql.scan(/join(?:\s+\w+)?\s+\S+\s+#{quoted_name}\son/).size
- end
-
- def instantiate(rows)
- rows.each_with_index do |row, i|
- primary_id = join_base.record_id(row)
- unless @base_records_hash[primary_id]
- @base_records_in_order << (@base_records_hash[primary_id] = join_base.instantiate(row))
- end
- construct(@base_records_hash[primary_id], @associations, join_associations.dup, row)
- end
- remove_duplicate_results!(join_base.active_record, @base_records_in_order, @associations)
- return @base_records_in_order
- end
-
- def remove_duplicate_results!(base, records, associations)
- case associations
- when Symbol, String
- reflection = base.reflections[associations]
- remove_uniq_by_reflection(reflection, records)
- when Array
- associations.each do |association|
- remove_duplicate_results!(base, records, association)
- end
- when Hash
- associations.keys.each do |name|
- reflection = base.reflections[name]
- remove_uniq_by_reflection(reflection, records)
-
- parent_records = []
- records.each do |record|
- if descendant = record.send(reflection.name)
- if reflection.collection?
- parent_records.concat descendant.target.uniq
- else
- parent_records << descendant
- end
- end
- end
-
- remove_duplicate_results!(reflection.klass, parent_records, associations[name]) unless parent_records.empty?
- end
- end
- end
-
- protected
-
- def build(associations, parent = nil, join_class = Arel::InnerJoin)
- parent ||= @joins.last
- case associations
- when Symbol, String
- reflection = parent.reflections[associations.to_s.intern] or
- raise ConfigurationError, "Association named '#{ associations }' was not found; perhaps you misspelled it?"
- @reflections << reflection
- @joins << build_join_association(reflection, parent).with_join_class(join_class)
- when Array
- associations.each do |association|
- build(association, parent, join_class)
- end
- when Hash
- associations.keys.sort{|a,b|a.to_s<=>b.to_s}.each do |name|
- build(name, parent, join_class)
- build(associations[name], nil, join_class)
- end
- else
- raise ConfigurationError, associations.inspect
- end
- end
-
- def remove_uniq_by_reflection(reflection, records)
- if reflection && reflection.collection?
- records.each { |record| record.send(reflection.name).target.uniq! }
- end
- end
-
- def build_join_association(reflection, parent)
- JoinAssociation.new(reflection, self, parent)
- end
-
- def construct(parent, associations, joins, row)
- case associations
- when Symbol, String
- 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|
- construct(parent, association, joins, row)
- end
- when Hash
- associations.keys.sort{|a,b|a.to_s<=>b.to_s}.each do |name|
- 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
- raise ConfigurationError, associations.inspect
- end
- end
-
- def construct_association(record, join, row)
- case join.reflection.macro
- when :has_many, :has_and_belongs_to_many
- collection = record.send(join.reflection.name)
- collection.loaded
-
- return nil if record.id.to_s != join.parent.record_id(row).to_s or row[join.aliased_primary_key].nil?
- association = join.instantiate(row)
- collection.target.push(association)
- collection.__send__(:set_inverse_instance, association, record)
- when :has_one
- return if record.id.to_s != join.parent.record_id(row).to_s
- return if record.instance_variable_defined?("@#{join.reflection.name}")
- association = join.instantiate(row) unless row[join.aliased_primary_key].nil?
- set_target_and_inverse(join, association, record)
- when :belongs_to
- return if record.id.to_s != join.parent.record_id(row).to_s or row[join.aliased_primary_key].nil?
- association = join.instantiate(row)
- set_target_and_inverse(join, association, record)
- else
- raise ConfigurationError, "unknown macro: #{join.reflection.macro}"
- end
- return association
- end
-
- def set_target_and_inverse(join, association, record)
- association_proxy = record.send("set_#{join.reflection.name}_target", association)
- association_proxy.__send__(:set_inverse_instance, association, record)
- end
-
- class JoinBase # :nodoc:
- attr_reader :active_record, :table_joins
- delegate :table_name, :column_names, :primary_key, :reflections, :sanitize_sql, :arel_engine, :to => :active_record
-
- def initialize(active_record, joins = nil)
- @active_record = active_record
- @cached_record = {}
- @table_joins = joins
- end
-
- def ==(other)
- other.class == self.class &&
- other.active_record == active_record &&
- other.table_joins == table_joins
- end
-
- def aliased_prefix
- "t0"
- end
-
- def aliased_primary_key
- "#{aliased_prefix}_r0"
- end
-
- def aliased_table_name
- active_record.table_name
- end
-
- def column_names_with_alias
- unless defined?(@column_names_with_alias)
- @column_names_with_alias = []
-
- ([primary_key] + (column_names - [primary_key])).each_with_index do |column_name, i|
- @column_names_with_alias << [column_name, "#{aliased_prefix}_r#{i}"]
- end
- end
-
- @column_names_with_alias
- end
-
- def extract_record(row)
- column_names_with_alias.inject({}){|record, (cn, an)| record[cn] = row[an]; record}
- end
-
- def record_id(row)
- row[aliased_primary_key]
- end
-
- def instantiate(row)
- @cached_record[record_id(row)] ||= active_record.send(:instantiate, extract_record(row))
- end
- end
-
- class JoinAssociation < JoinBase # :nodoc:
- attr_reader :reflection, :parent, :aliased_table_name, :aliased_prefix, :aliased_join_table_name, :parent_table_name, :join_class
- delegate :options, :klass, :through_reflection, :source_reflection, :to => :reflection
-
- def initialize(reflection, join_dependency, parent = nil)
- reflection.check_validity!
- if reflection.options[:polymorphic]
- raise EagerLoadPolymorphicError.new(reflection)
- end
-
- super(reflection.klass)
- @join_dependency = join_dependency
- @parent = parent
- @reflection = reflection
- @aliased_prefix = "t#{ join_dependency.joins.size }"
- @parent_table_name = parent.active_record.table_name
- @aliased_table_name = aliased_table_name_for(table_name)
- @join = nil
- @join_class = Arel::InnerJoin
-
- if reflection.macro == :has_and_belongs_to_many
- @aliased_join_table_name = aliased_table_name_for(reflection.options[:join_table], "_join")
- end
-
- if [:has_many, :has_one].include?(reflection.macro) && reflection.options[:through]
- @aliased_join_table_name = aliased_table_name_for(reflection.through_reflection.klass.table_name, "_join")
- end
- end
-
- def ==(other)
- other.class == self.class &&
- other.reflection == reflection &&
- other.parent == parent
- end
-
- def find_parent_in(other_join_dependency)
- other_join_dependency.joins.detect do |join|
- self.parent == join
- end
- end
-
- def with_join_class(join_class)
- @join_class = join_class
- self
- end
-
- def association_join
- return @join if @join
-
- aliased_table = Arel::Table.new(table_name, :as => @aliased_table_name, :engine => arel_engine)
- parent_table = Arel::Table.new(parent.table_name, :as => parent.aliased_table_name, :engine => arel_engine)
-
- @join = case reflection.macro
- when :has_and_belongs_to_many
- join_table = Arel::Table.new(options[:join_table], :as => aliased_join_table_name, :engine => arel_engine)
- fk = options[:foreign_key] || reflection.active_record.to_s.foreign_key
- klass_fk = options[:association_foreign_key] || klass.to_s.foreign_key
-
- [
- join_table[fk].eq(parent_table[reflection.active_record.primary_key]),
- aliased_table[klass.primary_key].eq(join_table[klass_fk])
- ]
- when :has_many, :has_one
- if reflection.options[:through]
- join_table = Arel::Table.new(through_reflection.klass.table_name, :as => aliased_join_table_name, :engine => arel_engine)
- jt_foreign_key = jt_as_extra = jt_source_extra = jt_sti_extra = nil
- first_key = second_key = as_extra = nil
-
- if through_reflection.options[:as] # has_many :through against a polymorphic join
- jt_foreign_key = through_reflection.options[:as].to_s + '_id'
- jt_as_extra = join_table[through_reflection.options[:as].to_s + '_type'].eq(parent.active_record.base_class.name)
- else
- jt_foreign_key = through_reflection.primary_key_name
- end
-
- case source_reflection.macro
- when :has_many
- if source_reflection.options[:as]
- first_key = "#{source_reflection.options[:as]}_id"
- second_key = options[:foreign_key] || primary_key
- as_extra = aliased_table["#{source_reflection.options[:as]}_type"].eq(source_reflection.active_record.base_class.name)
- else
- first_key = through_reflection.klass.base_class.to_s.foreign_key
- second_key = options[:foreign_key] || primary_key
- end
-
- unless through_reflection.klass.descends_from_active_record?
- jt_sti_extra = join_table[through_reflection.active_record.inheritance_column].eq(through_reflection.klass.sti_name)
- end
- when :belongs_to
- first_key = primary_key
- if reflection.options[:source_type]
- second_key = source_reflection.association_foreign_key
- jt_source_extra = join_table[reflection.source_reflection.options[:foreign_type]].eq(reflection.options[:source_type])
- else
- second_key = source_reflection.primary_key_name
- end
- end
-
- [
- [parent_table[parent.primary_key].eq(join_table[jt_foreign_key]), jt_as_extra, jt_source_extra, jt_sti_extra].reject{|x| x.blank? },
- aliased_table[first_key].eq(join_table[second_key])
- ]
- elsif reflection.options[:as]
- id_rel = aliased_table["#{reflection.options[:as]}_id"].eq(parent_table[parent.primary_key])
- type_rel = aliased_table["#{reflection.options[:as]}_type"].eq(parent.active_record.base_class.name)
- [id_rel, type_rel]
- else
- foreign_key = options[:foreign_key] || reflection.active_record.name.foreign_key
- [aliased_table[foreign_key].eq(parent_table[reflection.options[:primary_key] || parent.primary_key])]
- end
- when :belongs_to
- [aliased_table[options[:primary_key] || reflection.klass.primary_key].eq(parent_table[options[:foreign_key] || reflection.primary_key_name])]
- end
-
- unless klass.descends_from_active_record?
- sti_column = aliased_table[klass.inheritance_column]
- sti_condition = sti_column.eq(klass.sti_name)
- klass.descendants.each {|subclass| sti_condition = sti_condition.or(sti_column.eq(subclass.sti_name)) }
-
- @join << sti_condition
- end
-
- [through_reflection, reflection].each do |ref|
- if ref && ref.options[:conditions]
- @join << interpolate_sql(sanitize_sql(ref.options[:conditions], aliased_table_name))
- end
- end
-
- @join
- end
-
- def relation
- aliased = Arel::Table.new(table_name, :as => @aliased_table_name, :engine => arel_engine)
-
- if reflection.macro == :has_and_belongs_to_many
- [Arel::Table.new(options[:join_table], :as => aliased_join_table_name, :engine => arel_engine), aliased]
- elsif reflection.options[:through]
- [Arel::Table.new(through_reflection.klass.table_name, :as => aliased_join_table_name, :engine => arel_engine), aliased]
- else
- aliased
- end
- end
-
- def join_relation(joining_relation, join = nil)
- joining_relation.joins(self.with_join_class(Arel::OuterJoin))
- end
-
- protected
-
- def aliased_table_name_for(name, suffix = nil)
- if @join_dependency.table_aliases[name].zero?
- @join_dependency.table_aliases[name] = @join_dependency.count_aliases_from_table_joins(name)
- end
-
- if !@join_dependency.table_aliases[name].zero? # We need an alias
- name = active_record.connection.table_alias_for "#{pluralize(reflection.name)}_#{parent_table_name}#{suffix}"
- @join_dependency.table_aliases[name] += 1
- if @join_dependency.table_aliases[name] == 1 # First time we've seen this name
- # Also need to count the aliases from the table_aliases to avoid incorrect count
- @join_dependency.table_aliases[name] += @join_dependency.count_aliases_from_table_joins(name)
- end
- table_index = @join_dependency.table_aliases[name]
- name = name[0..active_record.connection.table_alias_length-3] + "_#{table_index}" if table_index > 1
- else
- @join_dependency.table_aliases[name] += 1
- end
-
- name
- end
-
- def pluralize(table_name)
- ActiveRecord::Base.pluralize_table_names ? table_name.to_s.pluralize : table_name
- end
-
- def table_alias_for(table_name, table_alias)
- "#{table_name} #{table_alias if table_name != table_alias}".strip
- end
-
- def table_name_and_alias
- table_alias_for table_name, @aliased_table_name
- end
-
- def interpolate_sql(sql)
- instance_eval("%@#{sql.gsub('@', '\@')}@", __FILE__, __LINE__)
- end
- end
- end
end
end
end
diff --git a/activerecord/lib/active_record/associations/alias_tracker.rb b/activerecord/lib/active_record/associations/alias_tracker.rb
new file mode 100644
index 0000000000..634dee2289
--- /dev/null
+++ b/activerecord/lib/active_record/associations/alias_tracker.rb
@@ -0,0 +1,85 @@
+require 'active_support/core_ext/string/conversions'
+
+module ActiveRecord
+ module Associations
+ # Keeps track of table aliases for ActiveRecord::Associations::ClassMethods::JoinDependency and
+ # ActiveRecord::Associations::ThroughAssociationScope
+ class AliasTracker # :nodoc:
+ attr_reader :aliases, :table_joins
+
+ # table_joins is an array of arel joins which might conflict with the aliases we assign here
+ def initialize(table_joins = [])
+ @aliases = Hash.new
+ @table_joins = table_joins
+ end
+
+ def aliased_table_for(table_name, aliased_name = nil)
+ table_alias = aliased_name_for(table_name, aliased_name)
+
+ if table_alias == table_name
+ Arel::Table.new(table_name)
+ else
+ Arel::Table.new(table_name).alias(table_alias)
+ end
+ end
+
+ def aliased_name_for(table_name, aliased_name = nil)
+ aliased_name ||= table_name
+
+ initialize_count_for(table_name) if aliases[table_name].nil?
+
+ if aliases[table_name].zero?
+ # If it's zero, we can have our table_name
+ aliases[table_name] = 1
+ table_name
+ else
+ # Otherwise, we need to use an alias
+ aliased_name = connection.table_alias_for(aliased_name)
+
+ initialize_count_for(aliased_name) if aliases[aliased_name].nil?
+
+ # Update the count
+ aliases[aliased_name] += 1
+
+ if aliases[aliased_name] > 1
+ "#{truncate(aliased_name)}_#{aliases[aliased_name]}"
+ else
+ aliased_name
+ end
+ end
+ end
+
+ def pluralize(table_name)
+ ActiveRecord::Base.pluralize_table_names ? table_name.to_s.pluralize : table_name
+ end
+
+ private
+
+ def initialize_count_for(name)
+ aliases[name] = 0
+
+ unless Arel::Table === table_joins
+ # quoted_name should be downcased as some database adapters (Oracle) return quoted name in uppercase
+ quoted_name = connection.quote_table_name(name).downcase
+
+ aliases[name] += table_joins.map { |join|
+ # Table names + table aliases
+ join.left.downcase.scan(
+ /join(?:\s+\w+)?\s+(\S+\s+)?#{quoted_name}\son/
+ ).size
+ }.sum
+ end
+
+ aliases[name]
+ end
+
+ def truncate(name)
+ name[0..connection.table_alias_length-3]
+ end
+
+ def connection
+ ActiveRecord::Base.connection
+ end
+ end
+ end
+end
diff --git a/activerecord/lib/active_record/associations/association.rb b/activerecord/lib/active_record/associations/association.rb
new file mode 100644
index 0000000000..27c446b12c
--- /dev/null
+++ b/activerecord/lib/active_record/associations/association.rb
@@ -0,0 +1,230 @@
+require 'active_support/core_ext/array/wrap'
+
+module ActiveRecord
+ module Associations
+ # = Active Record Associations
+ #
+ # This is the root class of all associations ('+ Foo' signifies an included module Foo):
+ #
+ # Association
+ # SingularAssociation
+ # HasOneAssociation
+ # HasOneThroughAssociation + ThroughAssociation
+ # BelongsToAssociation
+ # BelongsToPolymorphicAssociation
+ # CollectionAssociation
+ # HasAndBelongsToManyAssociation
+ # HasManyAssociation
+ # HasManyThroughAssociation + ThroughAssociation
+ class Association #:nodoc:
+ attr_reader :owner, :target, :reflection
+
+ delegate :options, :to => :reflection
+
+ def initialize(owner, reflection)
+ reflection.check_validity!
+
+ @target = nil
+ @owner, @reflection = owner, reflection
+ @updated = false
+
+ reset
+ construct_scope
+ end
+
+ # Returns the name of the table of the related class:
+ #
+ # post.comments.aliased_table_name # => "comments"
+ #
+ def aliased_table_name
+ reflection.klass.table_name
+ end
+
+ # Resets the \loaded flag to +false+ and sets the \target to +nil+.
+ def reset
+ @loaded = false
+ IdentityMap.remove(target) if IdentityMap.enabled? && target
+ @target = nil
+ end
+
+ # Reloads the \target and returns +self+ on success.
+ def reload
+ reset
+ construct_scope
+ load_target
+ self unless target.nil?
+ end
+
+ # Has the \target been already \loaded?
+ def loaded?
+ @loaded
+ end
+
+ # Asserts the \target has been loaded setting the \loaded flag to +true+.
+ def loaded!
+ @loaded = true
+ @stale_state = stale_state
+ end
+
+ # The target is stale if the target no longer points to the record(s) that the
+ # relevant foreign_key(s) refers to. If stale, the association accessor method
+ # on the owner will reload the target. It's up to subclasses to implement the
+ # state_state method if relevant.
+ #
+ # Note that if the target has not been loaded, it is not considered stale.
+ def stale_target?
+ loaded? && @stale_state != stale_state
+ end
+
+ # Sets the target of this association to <tt>\target</tt>, and the \loaded flag to +true+.
+ def target=(target)
+ @target = target
+ loaded!
+ end
+
+ def scoped
+ target_scope.merge(@association_scope)
+ end
+
+ # Construct the scope for this association.
+ #
+ # Note that the association_scope is merged into the target_scope only when the
+ # scoped method is called. This is because at that point the call may be surrounded
+ # by scope.scoping { ... } or with_scope { ... } etc, which affects the scope which
+ # actually gets built.
+ def construct_scope
+ if klass
+ @association_scope = AssociationScope.new(self).scope
+ end
+ end
+
+ # Set the inverse association, if possible
+ def set_inverse_instance(record)
+ if record && invertible_for?(record)
+ inverse = record.association(inverse_reflection_for(record).name)
+ inverse.target = owner
+ end
+ end
+
+ # This class of the target. belongs_to polymorphic overrides this to look at the
+ # polymorphic_type field on the owner.
+ def klass
+ reflection.klass
+ end
+
+ # Can be overridden (i.e. in ThroughAssociation) to merge in other scopes (i.e. the
+ # through association's scope)
+ def target_scope
+ klass.scoped
+ end
+
+ # Loads the \target if needed and returns it.
+ #
+ # This method is abstract in the sense that it relies on +find_target+,
+ # which is expected to be provided by descendants.
+ #
+ # If the \target is already \loaded it is just returned. Thus, you can call
+ # +load_target+ unconditionally to get the \target.
+ #
+ # ActiveRecord::RecordNotFound is rescued within the method, and it is
+ # not reraised. The proxy is \reset and +nil+ is the return value.
+ def load_target
+ if find_target?
+ begin
+ if IdentityMap.enabled? && association_class && association_class.respond_to?(:base_class)
+ @target = IdentityMap.get(association_class, owner[reflection.foreign_key])
+ end
+ rescue NameError
+ nil
+ ensure
+ @target ||= find_target
+ end
+ end
+ loaded!
+ target
+ rescue ActiveRecord::RecordNotFound
+ reset
+ end
+
+ private
+
+ def find_target?
+ !loaded? && (!owner.new_record? || foreign_key_present?) && klass
+ end
+
+ def interpolate(sql, record = nil)
+ if sql.respond_to?(:to_proc)
+ owner.send(:instance_exec, record, &sql)
+ else
+ sql
+ end
+ end
+
+ def creation_attributes
+ attributes = {}
+
+ if [:has_one, :has_many].include?(reflection.macro) && !options[:through]
+ attributes[reflection.foreign_key] = owner[reflection.active_record_primary_key]
+
+ if reflection.options[:as]
+ attributes[reflection.type] = owner.class.base_class.name
+ end
+ end
+
+ attributes
+ end
+
+ # Sets the owner attributes on the given record
+ def set_owner_attributes(record)
+ if owner.persisted?
+ creation_attributes.each { |key, value| record[key] = value }
+ end
+ end
+
+ # Should be true if there is a foreign key present on the owner which
+ # references the target. This is used to determine whether we can load
+ # the target if the owner is currently a new record (and therefore
+ # without a key).
+ #
+ # Currently implemented by belongs_to (vanilla and polymorphic) and
+ # has_one/has_many :through associations which go through a belongs_to
+ def foreign_key_present?
+ false
+ end
+
+ # Raises ActiveRecord::AssociationTypeMismatch unless +record+ is of
+ # the kind of the class of the associated objects. Meant to be used as
+ # a sanity check when you are about to assign an associated record.
+ def raise_on_type_mismatch(record)
+ unless record.is_a?(reflection.klass) || record.is_a?(reflection.class_name.constantize)
+ message = "#{reflection.class_name}(##{reflection.klass.object_id}) expected, got #{record.class}(##{record.class.object_id})"
+ raise ActiveRecord::AssociationTypeMismatch, message
+ end
+ end
+
+ # Can be redefined by subclasses, notably polymorphic belongs_to
+ # The record parameter is necessary to support polymorphic inverses as we must check for
+ # the association in the specific class of the record.
+ def inverse_reflection_for(record)
+ reflection.inverse_of
+ end
+
+ # Is this association invertible? Can be redefined by subclasses.
+ def invertible_for?(record)
+ inverse_reflection_for(record)
+ end
+
+ # This should be implemented to return the values of the relevant key(s) on the owner,
+ # so that when state_state is different from the value stored on the last find_target,
+ # the target is stale.
+ #
+ # This is only relevant to certain associations, which is why it returns nil by default.
+ def stale_state
+ end
+
+ def association_class
+ @reflection.klass
+ end
+ end
+ end
+end
diff --git a/activerecord/lib/active_record/associations/association_collection.rb b/activerecord/lib/active_record/associations/association_collection.rb
deleted file mode 100644
index b5159eead3..0000000000
--- a/activerecord/lib/active_record/associations/association_collection.rb
+++ /dev/null
@@ -1,557 +0,0 @@
-require 'set'
-require 'active_support/core_ext/array/wrap'
-
-module ActiveRecord
- module Associations
- # = Active Record Association Collection
- #
- # AssociationCollection is an abstract class that provides common stuff to
- # ease the implementation of association proxies that represent
- # collections. See the class hierarchy in AssociationProxy.
- #
- # You need to be careful with assumptions regarding the target: The proxy
- # does not fetch records from the database until it needs them, but new
- # ones created with +build+ are added to the target. So, the target may be
- # non-empty and still lack children waiting to be read from the database.
- # If you look directly to the database you cannot assume that's the entire
- # collection because new records may have been added to the target, etc.
- #
- # If you need to work on all current children, new and existing records,
- # +load_target+ and the +loaded+ flag are your friends.
- class AssociationCollection < AssociationProxy #:nodoc:
- def initialize(owner, reflection)
- super
- construct_sql
- end
-
- delegate :group, :order, :limit, :joins, :where, :preload, :eager_load, :includes, :from, :lock, :readonly, :having, :to => :scoped
-
- def select(select = nil)
- if block_given?
- load_target
- @target.select.each { |e| yield e }
- else
- scoped.select(select)
- end
- end
-
- def scoped
- with_scope(construct_scope) { @reflection.klass.scoped }
- end
-
- def find(*args)
- options = args.extract_options!
-
- # If using a custom finder_sql, scan the entire collection.
- if @reflection.options[:finder_sql]
- expects_array = args.first.kind_of?(Array)
- ids = args.flatten.compact.uniq.map { |arg| arg.to_i }
-
- if ids.size == 1
- id = ids.first
- record = load_target.detect { |r| id == r.id }
- expects_array ? [ record ] : record
- else
- load_target.select { |r| ids.include?(r.id) }
- end
- else
- merge_options_from_reflection!(options)
- construct_find_options!(options)
-
- find_scope = construct_scope[:find].slice(:conditions, :order)
-
- with_scope(:find => find_scope) do
- relation = @reflection.klass.send(:construct_finder_arel, options, @reflection.klass.send(:current_scoped_methods))
-
- case args.first
- when :first, :last
- relation.send(args.first)
- when :all
- records = relation.all
- @reflection.options[:uniq] ? uniq(records) : records
- else
- relation.find(*args)
- end
- end
- end
- end
-
- # Fetches the first one using SQL if possible.
- def first(*args)
- if fetch_first_or_last_using_find?(args)
- find(:first, *args)
- else
- load_target unless loaded?
- @target.first(*args)
- end
- end
-
- # Fetches the last one using SQL if possible.
- def last(*args)
- if fetch_first_or_last_using_find?(args)
- find(:last, *args)
- else
- load_target unless loaded?
- @target.last(*args)
- end
- end
-
- def to_ary
- load_target
- if @target.is_a?(Array)
- @target.to_ary
- else
- Array.wrap(@target)
- end
- end
- alias_method :to_a, :to_ary
-
- def reset
- reset_target!
- reset_named_scopes_cache!
- @loaded = false
- end
-
- def build(attributes = {}, &block)
- if attributes.is_a?(Array)
- attributes.collect { |attr| build(attr, &block) }
- else
- build_record(attributes) do |record|
- block.call(record) if block_given?
- set_belongs_to_association_for(record)
- end
- end
- end
-
- # Add +records+ to this association. Returns +self+ so method calls may be chained.
- # Since << flattens its argument list and inserts each record, +push+ and +concat+ behave identically.
- def <<(*records)
- result = true
- load_target if @owner.new_record?
-
- transaction do
- flatten_deeper(records).each do |record|
- raise_on_type_mismatch(record)
- add_record_to_target_with_callbacks(record) do |r|
- result &&= insert_record(record) unless @owner.new_record?
- end
- end
- end
-
- result && self
- end
-
- alias_method :push, :<<
- alias_method :concat, :<<
-
- # Starts a transaction in the association class's database connection.
- #
- # class Author < ActiveRecord::Base
- # has_many :books
- # end
- #
- # Author.first.books.transaction do
- # # same effect as calling Book.transaction
- # end
- def transaction(*args)
- @reflection.klass.transaction(*args) do
- yield
- end
- end
-
- # Remove all records from this association
- #
- # See delete for more info.
- def delete_all
- load_target
- delete(@target)
- reset_target!
- reset_named_scopes_cache!
- end
-
- # Calculate sum using SQL, not Enumerable
- def sum(*args)
- if block_given?
- calculate(:sum, *args) { |*block_args| yield(*block_args) }
- else
- calculate(:sum, *args)
- end
- end
-
- # Count all records using SQL. If the +:counter_sql+ option is set for the association, it will
- # be used for the query. If no +:counter_sql+ was supplied, but +:finder_sql+ was set, the
- # descendant's +construct_sql+ method will have set :counter_sql automatically.
- # Otherwise, construct options and pass them with scope to the target class's +count+.
- def count(column_name = nil, options = {})
- column_name, options = nil, column_name if column_name.is_a?(Hash)
-
- if @reflection.options[:counter_sql] && !options.blank?
- raise ArgumentError, "If finder_sql/counter_sql is used then options cannot be passed"
- elsif @reflection.options[:counter_sql]
- @reflection.klass.count_by_sql(@counter_sql)
- else
-
- if @reflection.options[:uniq]
- # This is needed because 'SELECT count(DISTINCT *)..' is not valid SQL.
- column_name = "#{@reflection.quoted_table_name}.#{@reflection.klass.primary_key}" unless column_name
- options.merge!(:distinct => true)
- end
-
- value = @reflection.klass.send(:with_scope, construct_scope) { @reflection.klass.count(column_name, options) }
-
- limit = @reflection.options[:limit]
- offset = @reflection.options[:offset]
-
- if limit || offset
- [ [value - offset.to_i, 0].max, limit.to_i ].min
- else
- value
- end
- end
- end
-
- # Removes +records+ from this association calling +before_remove+ and
- # +after_remove+ callbacks.
- #
- # This method is abstract in the sense that +delete_records+ has to be
- # provided by descendants. Note this method does not imply the records
- # are actually removed from the database, that depends precisely on
- # +delete_records+. They are in any case removed from the collection.
- def delete(*records)
- remove_records(records) do |_records, old_records|
- delete_records(old_records) if old_records.any?
- _records.each { |record| @target.delete(record) }
- end
- 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)
- records = find(records) if records.any? {|record| record.kind_of?(Fixnum) || record.kind_of?(String)}
- remove_records(records) do |_records, old_records|
- old_records.each { |record| record.destroy }
- end
-
- load_target
- end
-
- # Removes all records from this association. Returns +self+ so method calls may be chained.
- def clear
- return self if length.zero? # forces load_target if it hasn't happened already
-
- if @reflection.options[:dependent] && @reflection.options[:dependent] == :destroy
- destroy_all
- else
- delete_all
- end
-
- self
- end
-
- # Destroy all the records from this association.
- #
- # See destroy for more info.
- def destroy_all
- load_target
- destroy(@target).tap do
- reset_target!
- reset_named_scopes_cache!
- end
- end
-
- def create(attrs = {})
- if attrs.is_a?(Array)
- attrs.collect { |attr| create(attr) }
- else
- create_record(attrs) do |record|
- yield(record) if block_given?
- record.save
- end
- end
- end
-
- def create!(attrs = {})
- create_record(attrs) do |record|
- yield(record) if block_given?
- record.save!
- end
- end
-
- # Returns the size of the collection by executing a SELECT COUNT(*)
- # query if the collection hasn't been loaded, and calling
- # <tt>collection.size</tt> if it has.
- #
- # If the collection has been already loaded +size+ and +length+ are
- # equivalent. If not and you are going to need the records anyway
- # +length+ will take one less query. Otherwise +size+ is more efficient.
- #
- # This method is abstract in the sense that it relies on
- # +count_records+, which is a method descendants have to provide.
- def size
- if @owner.new_record? || (loaded? && !@reflection.options[:uniq])
- @target.size
- elsif !loaded? && @reflection.options[:group]
- load_target.size
- elsif !loaded? && !@reflection.options[:uniq] && @target.is_a?(Array)
- unsaved_records = @target.select { |r| r.new_record? }
- unsaved_records.size + count_records
- else
- count_records
- end
- end
-
- # Returns the size of the collection calling +size+ on the target.
- #
- # If the collection has been already loaded +length+ and +size+ are
- # equivalent. If not and you are going to need the records anyway this
- # method will take one less query. Otherwise +size+ is more efficient.
- def length
- load_target.size
- end
-
- # Equivalent to <tt>collection.size.zero?</tt>. If the collection has
- # not been already loaded and you are going to fetch the records anyway
- # it is better to check <tt>collection.length.zero?</tt>.
- def empty?
- size.zero?
- end
-
- def any?
- if block_given?
- method_missing(:any?) { |*block_args| yield(*block_args) }
- else
- !empty?
- end
- end
-
- # Returns true if the collection has more than 1 record. Equivalent to collection.size > 1.
- def many?
- if block_given?
- method_missing(:many?) { |*block_args| yield(*block_args) }
- else
- size > 1
- end
- end
-
- def uniq(collection = self)
- seen = Set.new
- collection.inject([]) do |kept, record|
- unless seen.include?(record.id)
- kept << record
- seen << record.id
- end
- kept
- end
- end
-
- # Replace this collection with +other_array+
- # This will perform a diff and delete/add only records that have changed.
- def replace(other_array)
- other_array.each { |val| raise_on_type_mismatch(val) }
-
- load_target
- other = other_array.size < 100 ? other_array : other_array.to_set
- current = @target.size < 100 ? @target : @target.to_set
-
- transaction do
- delete(@target.select { |v| !other.include?(v) })
- concat(other_array.select { |v| !current.include?(v) })
- end
- end
-
- def include?(record)
- return false unless record.is_a?(@reflection.klass)
- load_target if @reflection.options[:finder_sql] && !loaded?
- return @target.include?(record) if loaded?
- exists?(record)
- end
-
- def proxy_respond_to?(method, include_private = false)
- super || @reflection.klass.respond_to?(method, include_private)
- end
-
- protected
- def construct_find_options!(options)
- end
-
- def construct_counter_sql
- if @reflection.options[:counter_sql]
- @counter_sql = interpolate_sql(@reflection.options[:counter_sql])
- elsif @reflection.options[:finder_sql]
- # replace the SELECT clause with COUNT(*), preserving any hints within /* ... */
- @reflection.options[:counter_sql] = @reflection.options[:finder_sql].sub(/SELECT\b(\/\*.*?\*\/ )?(.*)\bFROM\b/im) { "SELECT #{$1}COUNT(*) FROM" }
- @counter_sql = interpolate_sql(@reflection.options[:counter_sql])
- else
- @counter_sql = @finder_sql
- end
- end
-
- def load_target
- if !@owner.new_record? || foreign_key_present
- begin
- if !loaded?
- if @target.is_a?(Array) && @target.any?
- @target = find_target.map do |f|
- i = @target.index(f)
- if i
- @target.delete_at(i).tap do |t|
- keys = ["id"] + t.changes.keys + (f.attribute_names - t.attribute_names)
- t.attributes = f.attributes.except(*keys)
- end
- else
- f
- end
- end + @target
- else
- @target = find_target
- end
- end
- rescue ActiveRecord::RecordNotFound
- reset
- end
- end
-
- loaded if target
- target
- end
-
- def method_missing(method, *args)
- match = DynamicFinderMatch.match(method)
- if match && match.creator?
- attributes = match.attribute_names
- return send(:"find_by_#{attributes.join('_and_')}", *args) || create(Hash[attributes.zip(args)])
- end
-
- if @target.respond_to?(method) || (!@reflection.klass.respond_to?(method) && Class.respond_to?(method))
- if block_given?
- super { |*block_args| yield(*block_args) }
- else
- super
- end
- elsif @reflection.klass.scopes[method]
- @_named_scopes_cache ||= {}
- @_named_scopes_cache[method] ||= {}
- @_named_scopes_cache[method][args] ||= with_scope(construct_scope) { @reflection.klass.send(method, *args) }
- else
- with_scope(construct_scope) do
- if block_given?
- @reflection.klass.send(method, *args) { |*block_args| yield(*block_args) }
- else
- @reflection.klass.send(method, *args)
- end
- end
- end
- end
-
- # overloaded in derived Association classes to provide useful scoping depending on association type.
- def construct_scope
- {}
- end
-
- def reset_target!
- @target = Array.new
- end
-
- def reset_named_scopes_cache!
- @_named_scopes_cache = {}
- end
-
- def find_target
- records =
- if @reflection.options[:finder_sql]
- @reflection.klass.find_by_sql(@finder_sql)
- else
- find(:all)
- end
-
- records = @reflection.options[:uniq] ? uniq(records) : records
- records.each do |record|
- set_inverse_instance(record, @owner)
- end
- records
- end
-
- def add_record_to_target_with_callbacks(record)
- callback(:before_add, record)
- yield(record) if block_given?
- @target ||= [] unless loaded?
- if index = @target.index(record)
- @target[index] = record
- else
- @target << record
- end
- callback(:after_add, record)
- set_inverse_instance(record, @owner)
- record
- end
-
- private
- def create_record(attrs)
- attrs.update(@reflection.options[:conditions]) if @reflection.options[:conditions].is_a?(Hash)
- ensure_owner_is_not_new
- record = @reflection.klass.send(:with_scope, :create => construct_scope[:create]) do
- @reflection.build_association(attrs)
- end
- if block_given?
- add_record_to_target_with_callbacks(record) { |*block_args| yield(*block_args) }
- else
- add_record_to_target_with_callbacks(record)
- end
- end
-
- def build_record(attrs)
- attrs.update(@reflection.options[:conditions]) if @reflection.options[:conditions].is_a?(Hash)
- record = @reflection.build_association(attrs)
- if block_given?
- add_record_to_target_with_callbacks(record) { |*block_args| yield(*block_args) }
- else
- add_record_to_target_with_callbacks(record)
- end
- 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|
- case callback
- when Symbol
- @owner.send(callback, record)
- when Proc
- callback.call(@owner, record)
- else
- callback.send(method, @owner, record)
- end
- end
- end
-
- def callbacks_for(callback_name)
- full_callback_name = "#{callback_name}_for_#{@reflection.name}"
- @owner.class.read_inheritable_attribute(full_callback_name.to_sym) || []
- end
-
- def ensure_owner_is_not_new
- if @owner.new_record?
- raise ActiveRecord::RecordNotSaved, "You cannot call create unless the parent is saved"
- end
- end
-
- def fetch_first_or_last_using_find?(args)
- args.first.kind_of?(Hash) || !(loaded? || @owner.new_record? || @reflection.options[:finder_sql] ||
- @target.any? { |record| record.new_record? } || args.first.kind_of?(Integer))
- end
- end
- end
-end
diff --git a/activerecord/lib/active_record/associations/association_proxy.rb b/activerecord/lib/active_record/associations/association_proxy.rb
deleted file mode 100644
index f333f4d603..0000000000
--- a/activerecord/lib/active_record/associations/association_proxy.rb
+++ /dev/null
@@ -1,295 +0,0 @@
-require 'active_support/core_ext/array/wrap'
-
-module ActiveRecord
- module Associations
- # = Active Record Associations
- #
- # This is the root class of all association proxies:
- #
- # AssociationProxy
- # BelongsToAssociation
- # HasOneAssociation
- # BelongsToPolymorphicAssociation
- # AssociationCollection
- # HasAndBelongsToManyAssociation
- # HasManyAssociation
- # HasManyThroughAssociation
- # HasOneThroughAssociation
- #
- # Association proxies in Active Record are middlemen between the object that
- # holds the association, known as the <tt>@owner</tt>, and the actual associated
- # object, known as the <tt>@target</tt>. The kind of association any proxy is
- # about is available in <tt>@reflection</tt>. That's an instance of the class
- # ActiveRecord::Reflection::AssociationReflection.
- #
- # For example, given
- #
- # class Blog < ActiveRecord::Base
- # has_many :posts
- # end
- #
- # blog = Blog.find(:first)
- #
- # the association proxy in <tt>blog.posts</tt> has the object in +blog+ as
- # <tt>@owner</tt>, the collection of its posts as <tt>@target</tt>, and
- # the <tt>@reflection</tt> object represents a <tt>:has_many</tt> macro.
- #
- # This class has most of the basic instance methods removed, and delegates
- # unknown methods to <tt>@target</tt> via <tt>method_missing</tt>. As a
- # corner case, it even removes the +class+ method and that's why you get
- #
- # blog.posts.class # => Array
- #
- # though the object behind <tt>blog.posts</tt> is not an Array, but an
- # ActiveRecord::Associations::HasManyAssociation.
- #
- # The <tt>@target</tt> object is not \loaded until needed. For example,
- #
- # blog.posts.count
- #
- # is computed directly through SQL and does not trigger by itself the
- # instantiation of the actual post records.
- class AssociationProxy #:nodoc:
- alias_method :proxy_respond_to?, :respond_to?
- alias_method :proxy_extend, :extend
- delegate :to_param, :to => :proxy_target
- instance_methods.each { |m| undef_method m unless m.to_s =~ /^(?:nil\?|send|object_id|to_a)$|^__|proxy_/ }
-
- def initialize(owner, reflection)
- @owner, @reflection = owner, reflection
- @updated = false
- reflection.check_validity!
- Array.wrap(reflection.options[:extend]).each { |ext| proxy_extend(ext) }
- reset
- end
-
- # Returns the owner of the proxy.
- def proxy_owner
- @owner
- end
-
- # Returns the reflection object that represents the association handled
- # by the proxy.
- def proxy_reflection
- @reflection
- end
-
- # Returns the \target of the proxy, same as +target+.
- def proxy_target
- @target
- end
-
- # Does the proxy or its \target respond to +symbol+?
- def respond_to?(*args)
- proxy_respond_to?(*args) || (load_target && @target.respond_to?(*args))
- end
-
- # Forwards <tt>===</tt> explicitly to the \target because the instance method
- # removal above doesn't catch it. Loads the \target if needed.
- def ===(other)
- load_target
- other === @target
- end
-
- # Returns the name of the table of the related class:
- #
- # post.comments.aliased_table_name # => "comments"
- #
- def aliased_table_name
- @reflection.klass.table_name
- end
-
- # Returns the SQL string that corresponds to the <tt>:conditions</tt>
- # option of the macro, if given, or +nil+ otherwise.
- def conditions
- @conditions ||= interpolate_sql(@reflection.sanitized_conditions) if @reflection.sanitized_conditions
- end
- alias :sql_conditions :conditions
-
- # Resets the \loaded flag to +false+ and sets the \target to +nil+.
- def reset
- @loaded = false
- @target = nil
- end
-
- # Reloads the \target and returns +self+ on success.
- def reload
- reset
- load_target
- self unless @target.nil?
- end
-
- # Has the \target been already \loaded?
- def loaded?
- @loaded
- end
-
- # Asserts the \target has been loaded setting the \loaded flag to +true+.
- def loaded
- @loaded = true
- end
-
- # Returns the target of this proxy, same as +proxy_target+.
- def target
- @target
- end
-
- # Sets the target of this proxy to <tt>\target</tt>, and the \loaded flag to +true+.
- def target=(target)
- @target = target
- loaded
- end
-
- # Forwards the call to the target. Loads the \target if needed.
- def inspect
- load_target
- @target.inspect
- end
-
- def send(method, *args)
- if proxy_respond_to?(method)
- super
- else
- load_target
- @target.send(method, *args)
- end
- end
-
- protected
- # Does the association have a <tt>:dependent</tt> option?
- def dependent?
- @reflection.options[:dependent]
- end
-
- def interpolate_sql(sql, record = nil)
- @owner.send(:interpolate_sql, sql, record)
- end
-
- # Forwards the call to the reflection class.
- def sanitize_sql(sql, table_name = @reflection.klass.table_name)
- @reflection.klass.send(:sanitize_sql, sql, table_name)
- end
-
- # Assigns the ID of the owner to the corresponding foreign key in +record+.
- # If the association is polymorphic the type of the owner is also set.
- def set_belongs_to_association_for(record)
- if @reflection.options[:as]
- record["#{@reflection.options[:as]}_id"] = @owner.id unless @owner.new_record?
- record["#{@reflection.options[:as]}_type"] = @owner.class.base_class.name.to_s
- else
- unless @owner.new_record?
- primary_key = @reflection.options[:primary_key] || :id
- record[@reflection.primary_key_name] = @owner.send(primary_key)
- end
- end
- end
-
- # Merges into +options+ the ones coming from the reflection.
- def merge_options_from_reflection!(options)
- options.reverse_merge!(
- :group => @reflection.options[:group],
- :having => @reflection.options[:having],
- :limit => @reflection.options[:limit],
- :offset => @reflection.options[:offset],
- :joins => @reflection.options[:joins],
- :include => @reflection.options[:include],
- :select => @reflection.options[:select],
- :readonly => @reflection.options[:readonly]
- )
- end
-
- # Forwards +with_scope+ to the reflection.
- def with_scope(*args, &block)
- @reflection.klass.send :with_scope, *args, &block
- end
-
- private
- # Forwards any missing method call to the \target.
- def method_missing(method, *args)
- if load_target
- unless @target.respond_to?(method)
- message = "undefined method `#{method.to_s}' for \"#{@target}\":#{@target.class.to_s}"
- raise NoMethodError, message
- end
-
- if block_given?
- @target.send(method, *args) { |*block_args| yield(*block_args) }
- else
- @target.send(method, *args)
- end
- end
- end
-
- # Loads the \target if needed and returns it.
- #
- # This method is abstract in the sense that it relies on +find_target+,
- # which is expected to be provided by descendants.
- #
- # If the \target is already \loaded it is just returned. Thus, you can call
- # +load_target+ unconditionally to get the \target.
- #
- # ActiveRecord::RecordNotFound is rescued within the method, and it is
- # not reraised. The proxy is \reset and +nil+ is the return value.
- def load_target
- return nil unless defined?(@loaded)
-
- if !loaded? and (!@owner.new_record? || foreign_key_present)
- @target = find_target
- end
-
- @loaded = true
- @target
- rescue ActiveRecord::RecordNotFound
- reset
- end
-
- # Can be overwritten by associations that might have the foreign key
- # available for an association without having the object itself (and
- # still being a new record). Currently, only +belongs_to+ presents
- # this scenario (both vanilla and polymorphic).
- def foreign_key_present
- false
- end
-
- # Raises ActiveRecord::AssociationTypeMismatch unless +record+ is of
- # the kind of the class of the associated objects. Meant to be used as
- # a sanity check when you are about to assign an associated record.
- def raise_on_type_mismatch(record)
- unless record.is_a?(@reflection.klass) || record.is_a?(@reflection.class_name.constantize)
- message = "#{@reflection.class_name}(##{@reflection.klass.object_id}) expected, got #{record.class}(##{record.class.object_id})"
- raise ActiveRecord::AssociationTypeMismatch, message
- end
- end
-
- if RUBY_VERSION < '1.9.2'
- # Array#flatten has problems with recursive arrays before Ruby 1.9.2.
- # Going one level deeper solves the majority of the problems.
- def flatten_deeper(array)
- array.collect { |element| (element.respond_to?(:flatten) && !element.is_a?(Hash)) ? element.flatten : element }.flatten
- end
- else
- def flatten_deeper(array)
- array.flatten
- end
- end
-
- # Returns the ID of the owner, quoted if needed.
- def owner_quoted_id
- @owner.quoted_id
- end
-
- def set_inverse_instance(record, instance)
- return if record.nil? || !we_can_set_the_inverse_on_this?(record)
- inverse_relationship = @reflection.inverse_of
- unless inverse_relationship.nil?
- record.send(:"set_#{inverse_relationship.name}_target", instance)
- end
- end
-
- # Override in subclasses
- def we_can_set_the_inverse_on_this?(record)
- false
- end
- end
- end
-end
diff --git a/activerecord/lib/active_record/associations/association_scope.rb b/activerecord/lib/active_record/associations/association_scope.rb
new file mode 100644
index 0000000000..ab102b2b8f
--- /dev/null
+++ b/activerecord/lib/active_record/associations/association_scope.rb
@@ -0,0 +1,120 @@
+module ActiveRecord
+ module Associations
+ class AssociationScope #:nodoc:
+ include JoinHelper
+
+ attr_reader :association, :alias_tracker
+
+ delegate :klass, :owner, :reflection, :interpolate, :to => :association
+ delegate :chain, :conditions, :options, :source_options, :active_record, :to => :reflection
+
+ def initialize(association)
+ @association = association
+ @alias_tracker = AliasTracker.new
+ end
+
+ def scope
+ scope = klass.unscoped
+ scope = scope.extending(*Array.wrap(options[:extend]))
+
+ # It's okay to just apply all these like this. The options will only be present if the
+ # association supports that option; this is enforced by the association builder.
+ scope = scope.apply_finder_options(options.slice(
+ :readonly, :include, :order, :limit, :joins, :group, :having, :offset))
+
+ if options[:through] && !options[:include]
+ scope = scope.includes(source_options[:include])
+ end
+
+ if select = select_value
+ scope = scope.select(select)
+ end
+
+ add_constraints(scope)
+ end
+
+ private
+
+ def select_value
+ select_value = options[:select]
+
+ if reflection.collection?
+ select_value ||= options[:uniq] && "DISTINCT #{reflection.quoted_table_name}.*"
+ end
+
+ if reflection.macro == :has_and_belongs_to_many
+ select_value ||= reflection.klass.arel_table[Arel.star]
+ end
+
+ select_value
+ end
+
+ def add_constraints(scope)
+ tables = construct_tables
+
+ chain.each_with_index do |reflection, i|
+ table, foreign_table = tables.shift, tables.first
+
+ if reflection.source_macro == :has_and_belongs_to_many
+ join_table = tables.shift
+
+ scope = scope.joins(join(
+ join_table,
+ table[reflection.active_record_primary_key].
+ eq(join_table[reflection.association_foreign_key])
+ ))
+
+ table, foreign_table = join_table, tables.first
+ end
+
+ if reflection.source_macro == :belongs_to
+ key = reflection.association_primary_key
+ foreign_key = reflection.foreign_key
+ else
+ key = reflection.foreign_key
+ foreign_key = reflection.active_record_primary_key
+ end
+
+ if reflection == chain.last
+ scope = scope.where(table[key].eq(owner[foreign_key]))
+
+ conditions[i].each do |condition|
+ if options[:through] && condition.is_a?(Hash)
+ condition = { table.name => condition }
+ end
+
+ scope = scope.where(interpolate(condition))
+ end
+ else
+ constraint = table[key].eq(foreign_table[foreign_key])
+ join = join(foreign_table, constraint)
+
+ scope = scope.joins(join)
+
+ unless conditions[i].empty?
+ scope = scope.where(sanitize(conditions[i], table))
+ end
+ end
+ end
+
+ scope
+ end
+
+ def alias_suffix
+ reflection.name
+ end
+
+ def table_name_for(reflection)
+ if reflection == self.reflection
+ # If this is a polymorphic belongs_to, we want to get the klass from the
+ # association because it depends on the polymorphic_type attribute of
+ # the owner
+ klass.table_name
+ else
+ reflection.table_name
+ end
+ end
+
+ end
+ end
+end
diff --git a/activerecord/lib/active_record/associations/belongs_to_association.rb b/activerecord/lib/active_record/associations/belongs_to_association.rb
index 4558872a2b..c263edd2c6 100644
--- a/activerecord/lib/active_record/associations/belongs_to_association.rb
+++ b/activerecord/lib/active_record/associations/belongs_to_association.rb
@@ -1,41 +1,17 @@
module ActiveRecord
# = Active Record Belongs To Associations
module Associations
- class BelongsToAssociation < AssociationProxy #:nodoc:
- def create(attributes = {})
- replace(@reflection.create_association(attributes))
- end
-
- def build(attributes = {})
- replace(@reflection.build_association(attributes))
- end
-
+ class BelongsToAssociation < SingularAssociation #:nodoc:
def replace(record)
- counter_cache_name = @reflection.counter_cache_column
-
- if record.nil?
- if counter_cache_name && !@owner.new_record?
- @reflection.klass.decrement_counter(counter_cache_name, previous_record_id) if @owner[@reflection.primary_key_name]
- end
-
- @target = @owner[@reflection.primary_key_name] = nil
- else
- raise_on_type_mismatch(record)
+ raise_on_type_mismatch(record) if record
- if counter_cache_name && !@owner.new_record? && record.id != @owner[@reflection.primary_key_name]
- @reflection.klass.increment_counter(counter_cache_name, record.id)
- @reflection.klass.decrement_counter(counter_cache_name, @owner[@reflection.primary_key_name]) if @owner[@reflection.primary_key_name]
- end
-
- @target = (AssociationProxy === record ? record.target : record)
- @owner[@reflection.primary_key_name] = record_id(record) unless record.new_record?
- @updated = true
- end
+ update_counters(record)
+ replace_keys(record)
+ set_inverse_instance(record)
- set_inverse_instance(record, @owner)
+ @updated = true if record
- loaded
- record
+ self.target = record
end
def updated?
@@ -43,44 +19,52 @@ module ActiveRecord
end
private
- def find_target
- find_method = if @reflection.options[:primary_key]
- "find_by_#{@reflection.options[:primary_key]}"
- else
- "find"
- end
- the_target = @reflection.klass.send(find_method,
- @owner[@reflection.primary_key_name],
- :select => @reflection.options[:select],
- :conditions => conditions,
- :include => @reflection.options[:include],
- :readonly => @reflection.options[:readonly]
- ) if @owner[@reflection.primary_key_name]
- set_inverse_instance(the_target, @owner)
- the_target
+
+ def update_counters(record)
+ counter_cache_name = reflection.counter_cache_column
+
+ if counter_cache_name && owner.persisted? && different_target?(record)
+ if record
+ record.class.increment_counter(counter_cache_name, record.id)
+ end
+
+ if foreign_key_present?
+ klass.decrement_counter(counter_cache_name, target_id)
+ end
+ end
end
- def foreign_key_present
- !@owner[@reflection.primary_key_name].nil?
+ # Checks whether record is different to the current target, without loading it
+ def different_target?(record)
+ record.nil? && owner[reflection.foreign_key] ||
+ record.id != owner[reflection.foreign_key]
+ end
+
+ def replace_keys(record)
+ owner[reflection.foreign_key] = record && record[reflection.association_primary_key]
+ end
+
+ def foreign_key_present?
+ owner[reflection.foreign_key]
end
# NOTE - for now, we're only supporting inverse setting from belongs_to back onto
# has_one associations.
- def we_can_set_the_inverse_on_this?(record)
- @reflection.has_inverse? && @reflection.inverse_of.macro == :has_one
+ def invertible_for?(record)
+ inverse = inverse_reflection_for(record)
+ inverse && inverse.macro == :has_one
end
- def record_id(record)
- record.send(@reflection.options[:primary_key] || :id)
+ def target_id
+ if options[:primary_key]
+ owner.send(reflection.name).try(:id)
+ else
+ owner[reflection.foreign_key]
+ end
end
- def previous_record_id
- @previous_record_id ||= if @reflection.options[:primary_key]
- previous_record = @owner.send(@reflection.name)
- previous_record.nil? ? nil : previous_record.id
- else
- @owner[@reflection.primary_key_name]
- end
+ def stale_state
+ owner[reflection.foreign_key].to_s
end
end
end
diff --git a/activerecord/lib/active_record/associations/belongs_to_polymorphic_association.rb b/activerecord/lib/active_record/associations/belongs_to_polymorphic_association.rb
index 38454ec242..1ca448236e 100644
--- a/activerecord/lib/active_record/associations/belongs_to_polymorphic_association.rb
+++ b/activerecord/lib/active_record/associations/belongs_to_polymorphic_association.rb
@@ -1,77 +1,33 @@
module ActiveRecord
# = Active Record Belongs To Polymorphic Association
module Associations
- class BelongsToPolymorphicAssociation < AssociationProxy #:nodoc:
- def replace(record)
- if record.nil?
- @target = @owner[@reflection.primary_key_name] = @owner[@reflection.options[:foreign_type]] = nil
- else
- @target = (AssociationProxy === record ? record.target : record)
-
- @owner[@reflection.primary_key_name] = record_id(record)
- @owner[@reflection.options[:foreign_type]] = record.class.base_class.name.to_s
-
- @updated = true
- end
-
- set_inverse_instance(record, @owner)
- loaded
- record
- end
-
- def updated?
- @updated
- end
-
+ class BelongsToPolymorphicAssociation < BelongsToAssociation #:nodoc:
private
- # NOTE - for now, we're only supporting inverse setting from belongs_to back onto
- # has_one associations.
- def we_can_set_the_inverse_on_this?(record)
- if @reflection.has_inverse?
- inverse_association = @reflection.polymorphic_inverse_of(record.class)
- inverse_association && inverse_association.macro == :has_one
- else
- false
- end
+ def replace_keys(record)
+ super
+ owner[reflection.foreign_type] = record && record.class.base_class.name
end
- def set_inverse_instance(record, instance)
- return if record.nil? || !we_can_set_the_inverse_on_this?(record)
- inverse_relationship = @reflection.polymorphic_inverse_of(record.class)
- unless inverse_relationship.nil?
- record.send(:"set_#{inverse_relationship.name}_target", instance)
- end
+ def different_target?(record)
+ super || record.class != klass
end
- def find_target
- return nil if association_class.nil?
-
- target =
- if @reflection.options[:conditions]
- association_class.find(
- @owner[@reflection.primary_key_name],
- :select => @reflection.options[:select],
- :conditions => conditions,
- :include => @reflection.options[:include]
- )
- else
- association_class.find(@owner[@reflection.primary_key_name], :select => @reflection.options[:select], :include => @reflection.options[:include])
- end
- set_inverse_instance(target, @owner)
- target
+ def inverse_reflection_for(record)
+ reflection.polymorphic_inverse_of(record.class)
end
- def foreign_key_present
- !@owner[@reflection.primary_key_name].nil?
+ def klass
+ type = owner[reflection.foreign_type]
+ type && type.constantize
end
- def record_id(record)
- record.send(@reflection.options[:primary_key] || :id)
+ def raise_on_type_mismatch(record)
+ # A polymorphic association cannot have a type mismatch, by definition
end
- def association_class
- @owner[@reflection.options[:foreign_type]] ? @owner[@reflection.options[:foreign_type]].constantize : nil
+ def stale_state
+ [super, owner[reflection.foreign_type].to_s]
end
end
end
diff --git a/activerecord/lib/active_record/associations/builder/association.rb b/activerecord/lib/active_record/associations/builder/association.rb
new file mode 100644
index 0000000000..96fca97440
--- /dev/null
+++ b/activerecord/lib/active_record/associations/builder/association.rb
@@ -0,0 +1,53 @@
+module ActiveRecord::Associations::Builder
+ class Association #:nodoc:
+ class_attribute :valid_options
+ self.valid_options = [:class_name, :foreign_key, :select, :conditions, :include, :extend, :readonly, :validate]
+
+ # Set by subclasses
+ class_attribute :macro
+
+ attr_reader :model, :name, :options, :reflection
+
+ def self.build(model, name, options)
+ new(model, name, options).build
+ end
+
+ def initialize(model, name, options)
+ @model, @name, @options = model, name, options
+ end
+
+ def build
+ validate_options
+ reflection = model.create_reflection(self.class.macro, name, options, model)
+ define_accessors
+ reflection
+ end
+
+ private
+
+ def validate_options
+ options.assert_valid_keys(self.class.valid_options)
+ end
+
+ def define_accessors
+ define_readers
+ define_writers
+ end
+
+ def define_readers
+ name = self.name
+
+ model.redefine_method(name) do |*params|
+ association(name).reader(*params)
+ end
+ end
+
+ def define_writers
+ name = self.name
+
+ model.redefine_method("#{name}=") do |value|
+ association(name).writer(value)
+ end
+ end
+ end
+end
diff --git a/activerecord/lib/active_record/associations/builder/belongs_to.rb b/activerecord/lib/active_record/associations/builder/belongs_to.rb
new file mode 100644
index 0000000000..964e7fddc8
--- /dev/null
+++ b/activerecord/lib/active_record/associations/builder/belongs_to.rb
@@ -0,0 +1,83 @@
+module ActiveRecord::Associations::Builder
+ class BelongsTo < SingularAssociation #:nodoc:
+ self.macro = :belongs_to
+
+ self.valid_options += [:foreign_type, :polymorphic, :touch]
+
+ def constructable?
+ !options[:polymorphic]
+ end
+
+ def build
+ reflection = super
+ add_counter_cache_callbacks(reflection) if options[:counter_cache]
+ add_touch_callbacks(reflection) if options[:touch]
+ configure_dependency
+ reflection
+ end
+
+ private
+
+ def add_counter_cache_callbacks(reflection)
+ cache_column = reflection.counter_cache_column
+ name = self.name
+
+ method_name = "belongs_to_counter_cache_after_create_for_#{name}"
+ model.redefine_method(method_name) do
+ record = send(name)
+ record.class.increment_counter(cache_column, record.id) unless record.nil?
+ end
+ model.after_create(method_name)
+
+ method_name = "belongs_to_counter_cache_before_destroy_for_#{name}"
+ model.redefine_method(method_name) do
+ record = send(name)
+ record.class.decrement_counter(cache_column, record.id) unless record.nil?
+ end
+ model.before_destroy(method_name)
+
+ model.send(:module_eval,
+ "#{reflection.class_name}.send(:attr_readonly,\"#{cache_column}\".intern) if defined?(#{reflection.class_name}) && #{reflection.class_name}.respond_to?(:attr_readonly)", __FILE__, __LINE__
+ )
+ end
+
+ def add_touch_callbacks(reflection)
+ name = self.name
+ method_name = "belongs_to_touch_after_save_or_destroy_for_#{name}"
+ touch = options[:touch]
+
+ model.redefine_method(method_name) do
+ record = send(name)
+
+ unless record.nil?
+ if touch == true
+ record.touch
+ else
+ record.touch(touch)
+ end
+ end
+ end
+
+ model.after_save(method_name)
+ model.after_touch(method_name)
+ model.after_destroy(method_name)
+ end
+
+ def configure_dependency
+ if options[:dependent]
+ unless [:destroy, :delete].include?(options[:dependent])
+ raise ArgumentError, "The :dependent option expects either :destroy or :delete (#{options[:dependent].inspect})"
+ end
+
+ method_name = "belongs_to_dependent_#{options[:dependent]}_for_#{name}"
+ model.send(:class_eval, <<-eoruby, __FILE__, __LINE__ + 1)
+ def #{method_name}
+ association = #{name}
+ association.#{options[:dependent]} if association
+ end
+ eoruby
+ model.after_destroy method_name
+ end
+ end
+ end
+end
diff --git a/activerecord/lib/active_record/associations/builder/collection_association.rb b/activerecord/lib/active_record/associations/builder/collection_association.rb
new file mode 100644
index 0000000000..f62209a226
--- /dev/null
+++ b/activerecord/lib/active_record/associations/builder/collection_association.rb
@@ -0,0 +1,75 @@
+module ActiveRecord::Associations::Builder
+ class CollectionAssociation < Association #:nodoc:
+ CALLBACKS = [:before_add, :after_add, :before_remove, :after_remove]
+
+ self.valid_options += [
+ :table_name, :order, :group, :having, :limit, :offset, :uniq, :finder_sql,
+ :counter_sql, :before_add, :after_add, :before_remove, :after_remove
+ ]
+
+ attr_reader :block_extension
+
+ def self.build(model, name, options, &extension)
+ new(model, name, options, &extension).build
+ end
+
+ def initialize(model, name, options, &extension)
+ super(model, name, options)
+ @block_extension = extension
+ end
+
+ def build
+ wrap_block_extension
+ reflection = super
+ CALLBACKS.each { |callback_name| define_callback(callback_name) }
+ reflection
+ end
+
+ def writable?
+ true
+ end
+
+ private
+
+ def wrap_block_extension
+ options[:extend] = Array.wrap(options[:extend])
+
+ if block_extension
+ silence_warnings do
+ model.parent.const_set(extension_module_name, Module.new(&block_extension))
+ end
+ options[:extend].push("#{model.parent}::#{extension_module_name}".constantize)
+ end
+ end
+
+ def extension_module_name
+ @extension_module_name ||= "#{model.to_s.demodulize}#{name.to_s.camelize}AssociationExtension"
+ end
+
+ def define_callback(callback_name)
+ full_callback_name = "#{callback_name}_for_#{name}"
+
+ # TODO : why do i need method_defined? I think its because of the inheritance chain
+ model.class_attribute full_callback_name.to_sym unless model.method_defined?(full_callback_name)
+ model.send("#{full_callback_name}=", Array.wrap(options[callback_name.to_sym]))
+ end
+
+ def define_readers
+ super
+
+ name = self.name
+ model.redefine_method("#{name.to_s.singularize}_ids") do
+ association(name).ids_reader
+ end
+ end
+
+ def define_writers
+ super
+
+ name = self.name
+ model.redefine_method("#{name.to_s.singularize}_ids=") do |ids|
+ association(name).ids_writer(ids)
+ end
+ end
+ end
+end
diff --git a/activerecord/lib/active_record/associations/builder/has_and_belongs_to_many.rb b/activerecord/lib/active_record/associations/builder/has_and_belongs_to_many.rb
new file mode 100644
index 0000000000..4b48757da7
--- /dev/null
+++ b/activerecord/lib/active_record/associations/builder/has_and_belongs_to_many.rb
@@ -0,0 +1,63 @@
+module ActiveRecord::Associations::Builder
+ class HasAndBelongsToMany < CollectionAssociation #:nodoc:
+ self.macro = :has_and_belongs_to_many
+
+ self.valid_options += [:join_table, :association_foreign_key, :delete_sql, :insert_sql]
+
+ def build
+ reflection = super
+ check_validity(reflection)
+ define_after_destroy_method
+ reflection
+ end
+
+ private
+
+ def define_after_destroy_method
+ name = self.name
+ model.send(:class_eval, <<-eoruby, __FILE__, __LINE__ + 1)
+ def #{after_destroy_method_name}
+ association(#{name.to_sym.inspect}).delete_all
+ end
+ eoruby
+ model.after_destroy after_destroy_method_name
+ end
+
+ def after_destroy_method_name
+ "has_and_belongs_to_many_after_destroy_for_#{name}"
+ end
+
+ # TODO: These checks should probably be moved into the Reflection, and we should not be
+ # redefining the options[:join_table] value - instead we should define a
+ # reflection.join_table method.
+ def check_validity(reflection)
+ if reflection.association_foreign_key == reflection.foreign_key
+ raise ActiveRecord::HasAndBelongsToManyAssociationForeignKeyNeeded.new(reflection)
+ end
+
+ reflection.options[:join_table] ||= join_table_name(
+ model.send(:undecorated_table_name, model.to_s),
+ model.send(:undecorated_table_name, reflection.class_name)
+ )
+
+ if model.connection.supports_primary_key? && (model.connection.primary_key(reflection.options[:join_table]) rescue false)
+ raise ActiveRecord::HasAndBelongsToManyAssociationWithPrimaryKeyError.new(reflection)
+ end
+ end
+
+ # Generates a join table name from two provided table names.
+ # The names in the join table names end up in lexicographic order.
+ #
+ # join_table_name("members", "clubs") # => "clubs_members"
+ # join_table_name("members", "special_clubs") # => "members_special_clubs"
+ def join_table_name(first_table_name, second_table_name)
+ if first_table_name < second_table_name
+ join_table = "#{first_table_name}_#{second_table_name}"
+ else
+ join_table = "#{second_table_name}_#{first_table_name}"
+ end
+
+ model.table_name_prefix + join_table + model.table_name_suffix
+ end
+ end
+end
diff --git a/activerecord/lib/active_record/associations/builder/has_many.rb b/activerecord/lib/active_record/associations/builder/has_many.rb
new file mode 100644
index 0000000000..77bb66228d
--- /dev/null
+++ b/activerecord/lib/active_record/associations/builder/has_many.rb
@@ -0,0 +1,63 @@
+module ActiveRecord::Associations::Builder
+ class HasMany < CollectionAssociation #:nodoc:
+ self.macro = :has_many
+
+ self.valid_options += [:primary_key, :dependent, :as, :through, :source, :source_type, :inverse_of]
+
+ def build
+ reflection = super
+ configure_dependency
+ reflection
+ end
+
+ private
+
+ def configure_dependency
+ if options[:dependent]
+ unless [:destroy, :delete_all, :nullify, :restrict].include?(options[:dependent])
+ raise ArgumentError, "The :dependent option expects either :destroy, :delete_all, " \
+ ":nullify or :restrict (#{options[:dependent].inspect})"
+ end
+
+ send("define_#{options[:dependent]}_dependency_method")
+ model.before_destroy dependency_method_name
+ end
+ end
+
+ def define_destroy_dependency_method
+ name = self.name
+ model.send(:define_method, dependency_method_name) do
+ send(name).each do |o|
+ # No point in executing the counter update since we're going to destroy the parent anyway
+ counter_method = ('belongs_to_counter_cache_before_destroy_for_' + self.class.name.downcase).to_sym
+ if o.respond_to?(counter_method)
+ class << o
+ self
+ end.send(:define_method, counter_method, Proc.new {})
+ end
+ end
+
+ send(name).delete_all
+ end
+ end
+
+ def define_delete_all_dependency_method
+ name = self.name
+ model.send(:define_method, dependency_method_name) do
+ send(name).delete_all
+ end
+ end
+ alias :define_nullify_dependency_method :define_delete_all_dependency_method
+
+ def define_restrict_dependency_method
+ name = self.name
+ model.send(:define_method, dependency_method_name) do
+ raise ActiveRecord::DeleteRestrictionError.new(name) unless send(name).empty?
+ end
+ end
+
+ def dependency_method_name
+ "has_many_dependent_for_#{name}"
+ end
+ end
+end
diff --git a/activerecord/lib/active_record/associations/builder/has_one.rb b/activerecord/lib/active_record/associations/builder/has_one.rb
new file mode 100644
index 0000000000..07ba5d088e
--- /dev/null
+++ b/activerecord/lib/active_record/associations/builder/has_one.rb
@@ -0,0 +1,61 @@
+module ActiveRecord::Associations::Builder
+ class HasOne < SingularAssociation #:nodoc:
+ self.macro = :has_one
+
+ self.valid_options += [:order, :as]
+
+ class_attribute :through_options
+ self.through_options = [:through, :source, :source_type]
+
+ def constructable?
+ !options[:through]
+ end
+
+ def build
+ reflection = super
+ configure_dependency unless options[:through]
+ reflection
+ end
+
+ private
+
+ def validate_options
+ valid_options = self.class.valid_options
+ valid_options += self.class.through_options if options[:through]
+ options.assert_valid_keys(valid_options)
+ end
+
+ def configure_dependency
+ if options[:dependent]
+ unless [:destroy, :delete, :nullify, :restrict].include?(options[:dependent])
+ raise ArgumentError, "The :dependent option expects either :destroy, :delete, " \
+ ":nullify or :restrict (#{options[:dependent].inspect})"
+ end
+
+ send("define_#{options[:dependent]}_dependency_method")
+ model.before_destroy dependency_method_name
+ end
+ end
+
+ def dependency_method_name
+ "has_one_dependent_#{options[:dependent]}_for_#{name}"
+ end
+
+ def define_destroy_dependency_method
+ model.send(:class_eval, <<-eoruby, __FILE__, __LINE__ + 1)
+ def #{dependency_method_name}
+ association(#{name.to_sym.inspect}).delete
+ end
+ eoruby
+ end
+ alias :define_delete_dependency_method :define_destroy_dependency_method
+ alias :define_nullify_dependency_method :define_destroy_dependency_method
+
+ def define_restrict_dependency_method
+ name = self.name
+ model.redefine_method(dependency_method_name) do
+ raise ActiveRecord::DeleteRestrictionError.new(name) unless send(name).nil?
+ end
+ end
+ end
+end
diff --git a/activerecord/lib/active_record/associations/builder/singular_association.rb b/activerecord/lib/active_record/associations/builder/singular_association.rb
new file mode 100644
index 0000000000..06a414b874
--- /dev/null
+++ b/activerecord/lib/active_record/associations/builder/singular_association.rb
@@ -0,0 +1,32 @@
+module ActiveRecord::Associations::Builder
+ class SingularAssociation < Association #:nodoc:
+ self.valid_options += [:remote, :dependent, :counter_cache, :primary_key, :inverse_of]
+
+ def constructable?
+ true
+ end
+
+ def define_accessors
+ super
+ define_constructors if constructable?
+ end
+
+ private
+
+ def define_constructors
+ name = self.name
+
+ model.redefine_method("build_#{name}") do |*params|
+ association(name).build(*params)
+ end
+
+ model.redefine_method("create_#{name}") do |*params|
+ association(name).create(*params)
+ end
+
+ model.redefine_method("create_#{name}!") do |*params|
+ association(name).create!(*params)
+ end
+ end
+ end
+end
diff --git a/activerecord/lib/active_record/associations/collection_association.rb b/activerecord/lib/active_record/associations/collection_association.rb
new file mode 100644
index 0000000000..9f4fc44cc6
--- /dev/null
+++ b/activerecord/lib/active_record/associations/collection_association.rb
@@ -0,0 +1,536 @@
+require 'active_support/core_ext/array/wrap'
+
+module ActiveRecord
+ module Associations
+ # = Active Record Association Collection
+ #
+ # AssociationCollection is an abstract class that provides common stuff to
+ # ease the implementation of association proxies that represent
+ # collections. See the class hierarchy in AssociationProxy.
+ #
+ # You need to be careful with assumptions regarding the target: The proxy
+ # does not fetch records from the database until it needs them, but new
+ # ones created with +build+ are added to the target. So, the target may be
+ # non-empty and still lack children waiting to be read from the database.
+ # If you look directly to the database you cannot assume that's the entire
+ # collection because new records may have been added to the target, etc.
+ #
+ # If you need to work on all current children, new and existing records,
+ # +load_target+ and the +loaded+ flag are your friends.
+ class CollectionAssociation < Association #:nodoc:
+ attr_reader :proxy
+
+ def initialize(owner, reflection)
+ # When scopes are created via method_missing on the proxy, they are stored so that
+ # any records fetched from the database are kept around for future use.
+ @scopes_cache = Hash.new do |hash, method|
+ hash[method] = { }
+ end
+
+ super
+
+ @proxy = CollectionProxy.new(self)
+ end
+
+ # Implements the reader method, e.g. foo.items for Foo.has_many :items
+ def reader(force_reload = false)
+ if force_reload
+ klass.uncached { reload }
+ elsif stale_target?
+ reload
+ end
+
+ proxy
+ end
+
+ # Implements the writer method, e.g. foo.items= for Foo.has_many :items
+ def writer(records)
+ replace(records)
+ end
+
+ # Implements the ids reader method, e.g. foo.item_ids for Foo.has_many :items
+ def ids_reader
+ if loaded? || options[:finder_sql]
+ load_target.map do |record|
+ record.send(reflection.association_primary_key)
+ end
+ else
+ column = "#{reflection.quoted_table_name}.#{reflection.association_primary_key}"
+
+ scoped.select(column).except(:includes).map! do |record|
+ record.send(reflection.association_primary_key)
+ end
+ end
+ end
+
+ # Implements the ids writer method, e.g. foo.item_ids= for Foo.has_many :items
+ def ids_writer(ids)
+ pk_column = reflection.primary_key_column
+ ids = Array.wrap(ids).reject { |id| id.blank? }
+ ids.map! { |i| pk_column.type_cast(i) }
+ replace(klass.find(ids).index_by { |r| r.id }.values_at(*ids))
+ end
+
+ def reset
+ @loaded = false
+ @target = []
+ @scopes_cache.clear
+ end
+
+ def select(select = nil)
+ if block_given?
+ load_target.select.each { |e| yield e }
+ else
+ scoped.select(select)
+ end
+ end
+
+ def find(*args)
+ if options[:finder_sql]
+ find_by_scan(*args)
+ else
+ scoped.find(*args)
+ end
+ end
+
+ def first(*args)
+ first_or_last(:first, *args)
+ end
+
+ def last(*args)
+ first_or_last(:last, *args)
+ end
+
+ def build(attributes = {}, &block)
+ build_or_create(attributes, :build, &block)
+ end
+
+ def create(attributes = {}, &block)
+ unless owner.persisted?
+ raise ActiveRecord::RecordNotSaved, "You cannot call create unless the parent is saved"
+ end
+
+ build_or_create(attributes, :create, &block)
+ end
+
+ def create!(attrs = {}, &block)
+ record = create(attrs, &block)
+ Array.wrap(record).each(&:save!)
+ record
+ end
+
+ # Add +records+ to this association. Returns +self+ so method calls may be chained.
+ # Since << flattens its argument list and inserts each record, +push+ and +concat+ behave identically.
+ def concat(*records)
+ result = true
+ load_target if owner.new_record?
+
+ transaction do
+ records.flatten.each do |record|
+ raise_on_type_mismatch(record)
+ add_to_target(record) do |r|
+ result &&= insert_record(record) unless owner.new_record?
+ end
+ end
+ end
+
+ result && records
+ end
+
+ # Starts a transaction in the association class's database connection.
+ #
+ # class Author < ActiveRecord::Base
+ # has_many :books
+ # end
+ #
+ # Author.first.books.transaction do
+ # # same effect as calling Book.transaction
+ # end
+ def transaction(*args)
+ reflection.klass.transaction(*args) do
+ yield
+ end
+ end
+
+ # Remove all records from this association
+ #
+ # See delete for more info.
+ def delete_all
+ delete(load_target).tap do
+ reset
+ loaded!
+ end
+ end
+
+ # Destroy all the records from this association.
+ #
+ # See destroy for more info.
+ def destroy_all
+ destroy(load_target).tap do
+ reset
+ loaded!
+ end
+ end
+
+ # Calculate sum using SQL, not Enumerable
+ def sum(*args)
+ if block_given?
+ scoped.sum(*args) { |*block_args| yield(*block_args) }
+ else
+ scoped.sum(*args)
+ end
+ end
+
+ # Count all records using SQL. If the +:counter_sql+ or +:finder_sql+ option is set for the
+ # association, it will be used for the query. Otherwise, construct options and pass them with
+ # scope to the target class's +count+.
+ def count(column_name = nil, count_options = {})
+ column_name, count_options = nil, column_name if column_name.is_a?(Hash)
+
+ if options[:counter_sql] || options[:finder_sql]
+ unless count_options.blank?
+ raise ArgumentError, "If finder_sql/counter_sql is used then options cannot be passed"
+ end
+
+ reflection.klass.count_by_sql(custom_counter_sql)
+ else
+ if options[:uniq]
+ # This is needed because 'SELECT count(DISTINCT *)..' is not valid SQL.
+ column_name ||= reflection.klass.primary_key
+ count_options.merge!(:distinct => true)
+ end
+
+ value = scoped.count(column_name, count_options)
+
+ limit = options[:limit]
+ offset = options[:offset]
+
+ if limit || offset
+ [ [value - offset.to_i, 0].max, limit.to_i ].min
+ else
+ value
+ end
+ end
+ end
+
+ # Removes +records+ from this association calling +before_remove+ and
+ # +after_remove+ callbacks.
+ #
+ # This method is abstract in the sense that +delete_records+ has to be
+ # provided by descendants. Note this method does not imply the records
+ # are actually removed from the database, that depends precisely on
+ # +delete_records+. They are in any case removed from the collection.
+ def delete(*records)
+ delete_or_destroy(records, options[:dependent])
+ 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)
+ records = find(records) if records.any? { |record| record.kind_of?(Fixnum) || record.kind_of?(String) }
+ delete_or_destroy(records, :destroy)
+ end
+
+ # Returns the size of the collection by executing a SELECT COUNT(*)
+ # query if the collection hasn't been loaded, and calling
+ # <tt>collection.size</tt> if it has.
+ #
+ # If the collection has been already loaded +size+ and +length+ are
+ # equivalent. If not and you are going to need the records anyway
+ # +length+ will take one less query. Otherwise +size+ is more efficient.
+ #
+ # This method is abstract in the sense that it relies on
+ # +count_records+, which is a method descendants have to provide.
+ def size
+ if owner.new_record? || (loaded? && !options[:uniq])
+ target.size
+ elsif !loaded? && options[:group]
+ load_target.size
+ elsif !loaded? && !options[:uniq] && target.is_a?(Array)
+ unsaved_records = target.select { |r| r.new_record? }
+ unsaved_records.size + count_records
+ else
+ count_records
+ end
+ end
+
+ # Returns the size of the collection calling +size+ on the target.
+ #
+ # If the collection has been already loaded +length+ and +size+ are
+ # equivalent. If not and you are going to need the records anyway this
+ # method will take one less query. Otherwise +size+ is more efficient.
+ def length
+ load_target.size
+ end
+
+ # Equivalent to <tt>collection.size.zero?</tt>. If the collection has
+ # not been already loaded and you are going to fetch the records anyway
+ # it is better to check <tt>collection.length.zero?</tt>.
+ def empty?
+ size.zero?
+ end
+
+ def any?
+ if block_given?
+ load_target.any? { |*block_args| yield(*block_args) }
+ else
+ !empty?
+ end
+ end
+
+ # Returns true if the collection has more than 1 record. Equivalent to collection.size > 1.
+ def many?
+ if block_given?
+ load_target.many? { |*block_args| yield(*block_args) }
+ else
+ size > 1
+ end
+ end
+
+ def uniq(collection = load_target)
+ seen = {}
+ collection.find_all do |record|
+ seen[record.id] = true unless seen.key?(record.id)
+ end
+ end
+
+ # Replace this collection with +other_array+
+ # This will perform a diff and delete/add only records that have changed.
+ def replace(other_array)
+ other_array.each { |val| raise_on_type_mismatch(val) }
+ original_target = load_target.dup
+
+ transaction do
+ delete(target - other_array)
+
+ unless concat(other_array - target)
+ @target = original_target
+ raise RecordNotSaved, "Failed to replace #{reflection.name} because one or more of the " \
+ "new records could not be saved."
+ end
+ end
+ end
+
+ def include?(record)
+ if record.is_a?(reflection.klass)
+ if record.new_record?
+ include_in_memory?(record)
+ else
+ load_target if options[:finder_sql]
+ loaded? ? target.include?(record) : scoped.exists?(record)
+ end
+ else
+ false
+ end
+ end
+
+ def cached_scope(method, args)
+ @scopes_cache[method][args] ||= scoped.readonly(nil).send(method, *args)
+ end
+
+ def load_target
+ if find_target?
+ targets = []
+
+ begin
+ targets = find_target
+ rescue ActiveRecord::RecordNotFound
+ reset
+ end
+
+ @target = merge_target_lists(targets, target)
+ end
+
+ loaded!
+ target
+ end
+
+ def add_to_target(record)
+ transaction do
+ callback(:before_add, record)
+ yield(record) if block_given?
+
+ if options[:uniq] && index = @target.index(record)
+ @target[index] = record
+ else
+ @target << record
+ end
+
+ callback(:after_add, record)
+ set_inverse_instance(record)
+ end
+
+ record
+ end
+
+ private
+
+ def custom_counter_sql
+ if options[:counter_sql]
+ interpolate(options[:counter_sql])
+ else
+ # replace the SELECT clause with COUNT(*), preserving any hints within /* ... */
+ interpolate(options[:finder_sql]).sub(/SELECT\b(\/\*.*?\*\/ )?(.*)\bFROM\b/im) { "SELECT #{$1}COUNT(*) FROM" }
+ end
+ end
+
+ def custom_finder_sql
+ interpolate(options[:finder_sql])
+ end
+
+ def find_target
+ records =
+ if options[:finder_sql]
+ reflection.klass.find_by_sql(custom_finder_sql)
+ else
+ find(:all)
+ end
+
+ records = options[:uniq] ? uniq(records) : records
+ records.each { |record| set_inverse_instance(record) }
+ records
+ end
+
+ def merge_target_lists(loaded, existing)
+ return loaded if existing.empty?
+ return existing if loaded.empty?
+
+ loaded.map do |f|
+ i = existing.index(f)
+ if i
+ existing.delete_at(i).tap do |t|
+ keys = ["id"] + t.changes.keys + (f.attribute_names - t.attribute_names)
+ # FIXME: this call to attributes causes many NoMethodErrors
+ attributes = f.attributes
+ (attributes.keys - keys).each do |k|
+ t.send("#{k}=", attributes[k])
+ end
+ end
+ else
+ f
+ end
+ end + existing
+ end
+
+ def build_or_create(attributes, method)
+ records = Array.wrap(attributes).map do |attrs|
+ record = build_record(attrs)
+
+ add_to_target(record) do
+ yield(record) if block_given?
+ insert_record(record) if method == :create
+ end
+ end
+
+ attributes.is_a?(Array) ? records : records.first
+ end
+
+ # Do the relevant stuff to insert the given record into the association collection.
+ def insert_record(record, validate = true)
+ raise NotImplementedError
+ end
+
+ def build_record(attributes)
+ reflection.build_association(scoped.scope_for_create.merge(attributes))
+ end
+
+ def delete_or_destroy(records, method)
+ records = records.flatten
+ records.each { |record| raise_on_type_mismatch(record) }
+ existing_records = records.reject { |r| r.new_record? }
+
+ transaction do
+ records.each { |record| callback(:before_remove, record) }
+
+ delete_records(existing_records, method) if existing_records.any?
+ records.each { |record| target.delete(record) }
+
+ records.each { |record| callback(:after_remove, record) }
+ end
+ end
+
+ # Delete the given records from the association, using one of the methods :destroy,
+ # :delete_all or :nullify (or nil, in which case a default is used).
+ def delete_records(records, method)
+ raise NotImplementedError
+ end
+
+ def callback(method, record)
+ callbacks_for(method).each do |callback|
+ case callback
+ when Symbol
+ owner.send(callback, record)
+ when Proc
+ callback.call(owner, record)
+ else
+ callback.send(method, owner, record)
+ end
+ end
+ end
+
+ def callbacks_for(callback_name)
+ full_callback_name = "#{callback_name}_for_#{reflection.name}"
+ owner.class.send(full_callback_name.to_sym) || []
+ end
+
+ # Should we deal with assoc.first or assoc.last by issuing an independent query to
+ # the database, or by getting the target, and then taking the first/last item from that?
+ #
+ # If the args is just a non-empty options hash, go to the database.
+ #
+ # Otherwise, go to the database only if none of the following are true:
+ # * target already loaded
+ # * owner is new record
+ # * custom :finder_sql exists
+ # * target contains new or changed record(s)
+ # * the first arg is an integer (which indicates the number of records to be returned)
+ def fetch_first_or_last_using_find?(args)
+ if args.first.is_a?(Hash)
+ true
+ else
+ !(loaded? ||
+ owner.new_record? ||
+ options[:finder_sql] ||
+ target.any? { |record| record.new_record? || record.changed? } ||
+ args.first.kind_of?(Integer))
+ end
+ end
+
+ def include_in_memory?(record)
+ if reflection.is_a?(ActiveRecord::Reflection::ThroughReflection)
+ owner.send(reflection.through_reflection.name).any? { |source|
+ target = source.send(reflection.source_reflection.name)
+ target.respond_to?(:include?) ? target.include?(record) : target == record
+ } || target.include?(record)
+ else
+ target.include?(record)
+ end
+ end
+
+ # If using a custom finder_sql, #find scans the entire collection.
+ def find_by_scan(*args)
+ expects_array = args.first.kind_of?(Array)
+ ids = args.flatten.compact.uniq.map { |arg| arg.to_i }
+
+ if ids.size == 1
+ id = ids.first
+ record = load_target.detect { |r| id == r.id }
+ expects_array ? [ record ] : record
+ else
+ load_target.select { |r| ids.include?(r.id) }
+ end
+ end
+
+ # Fetches the first/last using SQL if possible, otherwise from the target array.
+ def first_or_last(type, *args)
+ args.shift if args.first.is_a?(Hash) && args.first.empty?
+
+ collection = fetch_first_or_last_using_find?(args) ? scoped : load_target
+ collection.send(type, *args)
+ end
+ end
+ end
+end
diff --git a/activerecord/lib/active_record/associations/collection_proxy.rb b/activerecord/lib/active_record/associations/collection_proxy.rb
new file mode 100644
index 0000000000..cf77d770c9
--- /dev/null
+++ b/activerecord/lib/active_record/associations/collection_proxy.rb
@@ -0,0 +1,127 @@
+module ActiveRecord
+ module Associations
+ # Association proxies in Active Record are middlemen between the object that
+ # holds the association, known as the <tt>@owner</tt>, and the actual associated
+ # object, known as the <tt>@target</tt>. The kind of association any proxy is
+ # about is available in <tt>@reflection</tt>. That's an instance of the class
+ # ActiveRecord::Reflection::AssociationReflection.
+ #
+ # For example, given
+ #
+ # class Blog < ActiveRecord::Base
+ # has_many :posts
+ # end
+ #
+ # blog = Blog.find(:first)
+ #
+ # the association proxy in <tt>blog.posts</tt> has the object in +blog+ as
+ # <tt>@owner</tt>, the collection of its posts as <tt>@target</tt>, and
+ # the <tt>@reflection</tt> object represents a <tt>:has_many</tt> macro.
+ #
+ # This class has most of the basic instance methods removed, and delegates
+ # unknown methods to <tt>@target</tt> via <tt>method_missing</tt>. As a
+ # corner case, it even removes the +class+ method and that's why you get
+ #
+ # blog.posts.class # => Array
+ #
+ # though the object behind <tt>blog.posts</tt> is not an Array, but an
+ # ActiveRecord::Associations::HasManyAssociation.
+ #
+ # The <tt>@target</tt> object is not \loaded until needed. For example,
+ #
+ # blog.posts.count
+ #
+ # is computed directly through SQL and does not trigger by itself the
+ # instantiation of the actual post records.
+ class CollectionProxy # :nodoc:
+ alias :proxy_extend :extend
+
+ instance_methods.each { |m| undef_method m unless m.to_s =~ /^(?:nil\?|send|object_id|to_a)$|^__|^respond_to|proxy_/ }
+
+ delegate :group, :order, :limit, :joins, :where, :preload, :eager_load, :includes, :from,
+ :lock, :readonly, :having, :to => :scoped
+
+ delegate :target, :load_target, :loaded?, :scoped,
+ :to => :@association
+
+ delegate :select, :find, :first, :last,
+ :build, :create, :create!,
+ :concat, :delete_all, :destroy_all, :delete, :destroy, :uniq,
+ :sum, :count, :size, :length, :empty?,
+ :any?, :many?, :include?,
+ :to => :@association
+
+ def initialize(association)
+ @association = association
+ Array.wrap(association.options[:extend]).each { |ext| proxy_extend(ext) }
+ end
+
+ def respond_to?(*args)
+ super ||
+ (load_target && target.respond_to?(*args)) ||
+ @association.klass.respond_to?(*args)
+ end
+
+ def method_missing(method, *args, &block)
+ match = DynamicFinderMatch.match(method)
+ if match && match.creator?
+ attributes = match.attribute_names
+ return send(:"find_by_#{attributes.join('_and_')}", *args) || create(Hash[attributes.zip(args)])
+ end
+
+ if target.respond_to?(method) || (!@association.klass.respond_to?(method) && Class.respond_to?(method))
+ if load_target
+ if target.respond_to?(method)
+ target.send(method, *args, &block)
+ else
+ begin
+ super
+ rescue NoMethodError => e
+ raise e, e.message.sub(/ for #<.*$/, " via proxy for #{target}")
+ end
+ end
+ end
+
+ elsif @association.klass.scopes[method]
+ @association.cached_scope(method, args)
+ else
+ scoped.readonly(nil).send(method, *args, &block)
+ end
+ end
+
+ # Forwards <tt>===</tt> explicitly to the \target because the instance method
+ # removal above doesn't catch it. Loads the \target if needed.
+ def ===(other)
+ other === load_target
+ end
+
+ def to_ary
+ load_target.dup
+ end
+ alias_method :to_a, :to_ary
+
+ def <<(*records)
+ @association.concat(records) && self
+ end
+ alias_method :push, :<<
+
+ def clear
+ delete_all
+ self
+ end
+
+ def reload
+ @association.reload
+ self
+ end
+
+ def new(*args, &block)
+ if @association.is_a?(HasManyThroughAssociation)
+ @association.build(*args, &block)
+ else
+ method_missing(:new, *args, &block)
+ end
+ end
+ end
+ end
+end
diff --git a/activerecord/lib/active_record/associations/has_and_belongs_to_many_association.rb b/activerecord/lib/active_record/associations/has_and_belongs_to_many_association.rb
index bec123e7a2..217213808b 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
@@ -1,136 +1,51 @@
module ActiveRecord
# = Active Record Has And Belongs To Many Association
module Associations
- class HasAndBelongsToManyAssociation < AssociationCollection #:nodoc:
- def create(attributes = {})
- create_record(attributes) { |record| insert_record(record) }
- end
+ class HasAndBelongsToManyAssociation < CollectionAssociation #:nodoc:
+ attr_reader :join_table
- def create!(attributes = {})
- create_record(attributes) { |record| insert_record(record, true) }
+ def initialize(owner, reflection)
+ @join_table = Arel::Table.new(reflection.options[:join_table])
+ super
end
- def columns
- @reflection.columns(@reflection.options[:join_table], "#{@reflection.options[:join_table]} Columns")
- end
+ def insert_record(record, validate = true)
+ return if record.new_record? && !record.save(:validate => validate)
- def reset_column_information
- @reflection.reset_column_information
- end
+ if options[:insert_sql]
+ owner.connection.insert(interpolate(options[:insert_sql], record))
+ else
+ stmt = join_table.compile_insert(
+ join_table[reflection.foreign_key] => owner.id,
+ join_table[reflection.association_foreign_key] => record.id
+ )
+
+ owner.connection.insert stmt.to_sql
+ end
- def has_primary_key?
- @has_primary_key ||= @owner.connection.supports_primary_key? && @owner.connection.primary_key(@reflection.options[:join_table])
+ record
end
- protected
- def construct_find_options!(options)
- options[:joins] = @join_sql
- options[:readonly] = finding_with_ambiguous_select?(options[:select] || @reflection.options[:select])
- options[:select] ||= (@reflection.options[:select] || '*')
- end
+ private
def count_records
load_target.size
end
- def insert_record(record, force = true, validate = true)
- if record.new_record?
- if force
- record.save!
- else
- return false unless record.save(:validate => validate)
- end
- end
-
- if @reflection.options[:insert_sql]
- @owner.connection.insert(interpolate_sql(@reflection.options[:insert_sql], record))
+ def delete_records(records, method)
+ if sql = options[:delete_sql]
+ records.each { |record| owner.connection.delete(interpolate(sql, record)) }
else
- relation = Arel::Table.new(@reflection.options[:join_table])
- timestamps = record_timestamp_columns(record)
- timezone = record.send(:current_time_from_proper_timezone) if timestamps.any?
-
- attributes = columns.inject({}) do |attrs, column|
- name = column.name
- case name.to_s
- when @reflection.primary_key_name.to_s
- attrs[relation[name]] = @owner.id
- when @reflection.association_foreign_key.to_s
- attrs[relation[name]] = record.id
- when *timestamps
- attrs[relation[name]] = timezone
- else
- if record.has_attribute?(name)
- value = @owner.send(:quote_value, record[name], column)
- attrs[relation[name]] = value unless value.nil?
- end
- end
- attrs
- end
-
- relation.insert(attributes)
- end
-
- return true
- end
-
- def delete_records(records)
- if sql = @reflection.options[:delete_sql]
- records.each { |record| @owner.connection.delete(interpolate_sql(sql, record)) }
- else
- relation = Arel::Table.new(@reflection.options[:join_table])
- relation.where(relation[@reflection.primary_key_name].eq(@owner.id).
- and(Arel::Predicates::In.new(relation[@reflection.association_foreign_key], records.map(&:id)))
- ).delete
+ relation = join_table
+ stmt = relation.where(relation[reflection.foreign_key].eq(owner.id).
+ and(relation[reflection.association_foreign_key].in(records.map { |x| x.id }.compact))
+ ).compile_delete
+ owner.connection.delete stmt.to_sql
end
end
- def construct_sql
- if @reflection.options[:finder_sql]
- @finder_sql = interpolate_sql(@reflection.options[:finder_sql])
- else
- @finder_sql = "#{@owner.connection.quote_table_name @reflection.options[:join_table]}.#{@reflection.primary_key_name} = #{owner_quoted_id} "
- @finder_sql << " AND (#{conditions})" if conditions
- end
-
- @join_sql = "INNER JOIN #{@owner.connection.quote_table_name @reflection.options[:join_table]} ON #{@reflection.quoted_table_name}.#{@reflection.klass.primary_key} = #{@owner.connection.quote_table_name @reflection.options[:join_table]}.#{@reflection.association_foreign_key}"
-
- construct_counter_sql
- end
-
- def construct_scope
- { :find => { :conditions => @finder_sql,
- :joins => @join_sql,
- :readonly => false,
- :order => @reflection.options[:order],
- :include => @reflection.options[:include],
- :limit => @reflection.options[:limit] } }
- end
-
- # Join tables with additional columns on top of the two foreign keys must be considered
- # ambiguous unless a select clause has been explicitly defined. Otherwise you can get
- # broken records back, if, for example, the join column also has an id column. This will
- # then overwrite the id column of the records coming back.
- def finding_with_ambiguous_select?(select_clause)
- !select_clause && columns.size != 2
- end
-
- private
- def create_record(attributes, &block)
- # Can't use Base.create because the foreign key may be a protected attribute.
- ensure_owner_is_not_new
- if attributes.is_a?(Array)
- attributes.collect { |attr| create(attr) }
- else
- build_record(attributes, &block)
- end
- end
-
- def record_timestamp_columns(record)
- if record.record_timestamps
- record.send(:all_timestamp_attributes).map(&:to_s)
- else
- []
- end
+ def invertible_for?(record)
+ false
end
end
end
diff --git a/activerecord/lib/active_record/associations/has_many_association.rb b/activerecord/lib/active_record/associations/has_many_association.rb
index c33bc6aa47..78c5c4b870 100644
--- a/activerecord/lib/active_record/associations/has_many_association.rb
+++ b/activerecord/lib/active_record/associations/has_many_association.rb
@@ -5,19 +5,14 @@ module ActiveRecord
#
# If the association has a <tt>:through</tt> option further specialization
# is provided by its child HasManyThroughAssociation.
- class HasManyAssociation < AssociationCollection #:nodoc:
- def initialize(owner, reflection)
- @finder_sql = nil
- super
+ class HasManyAssociation < CollectionAssociation #:nodoc:
+
+ def insert_record(record, validate = true)
+ set_owner_attributes(record)
+ record.save(:validate => validate)
end
- protected
- def owner_quoted_id
- if @reflection.options[:primary_key]
- quote_value(@owner.send(@reflection.options[:primary_key]))
- else
- @owner.quoted_id
- end
- end
+
+ private
# Returns the number of records in this collection.
#
@@ -34,94 +29,70 @@ module ActiveRecord
# the loaded flag is set to true as well.
def count_records
count = if has_cached_counter?
- @owner.send(:read_attribute, cached_counter_attribute_name)
- elsif @reflection.options[:counter_sql]
- @reflection.klass.count_by_sql(@counter_sql)
+ owner.send(:read_attribute, cached_counter_attribute_name)
+ elsif options[:counter_sql] || options[:finder_sql]
+ reflection.klass.count_by_sql(custom_counter_sql)
else
- @reflection.klass.count(:conditions => @counter_sql, :include => @reflection.options[:include])
+ scoped.count
end
# If there's nothing in the database and @target has no new records
# we are certain the current target is an empty array. This is a
# documented side-effect of the method that may avoid an extra SELECT.
- @target ||= [] and loaded if count == 0
-
- if @reflection.options[:limit]
- count = [ @reflection.options[:limit], count ].min
- end
-
- return count
- end
+ @target ||= [] and loaded! if count == 0
- def has_cached_counter?
- @owner.attribute_present?(cached_counter_attribute_name)
+ [options[:limit], count].compact.min
end
- def cached_counter_attribute_name
- "#{@reflection.name}_count"
+ def has_cached_counter?(reflection = reflection)
+ owner.attribute_present?(cached_counter_attribute_name(reflection))
end
- def insert_record(record, force = false, validate = true)
- set_belongs_to_association_for(record)
- force ? record.save! : record.save(:validate => validate)
+ def cached_counter_attribute_name(reflection = reflection)
+ "#{reflection.name}_count"
end
- # Deletes the records according to the <tt>:dependent</tt> option.
- def delete_records(records)
- case @reflection.options[:dependent]
- when :destroy
- records.each { |r| r.destroy }
- when :delete_all
- @reflection.klass.delete(records.map { |record| record.id })
- else
- relation = Arel::Table.new(@reflection.table_name)
- relation.where(relation[@reflection.primary_key_name].eq(@owner.id).
- and(Arel::Predicates::In.new(relation[@reflection.klass.primary_key], records.map(&:id)))
- ).update(relation[@reflection.primary_key_name] => nil)
-
- @owner.class.update_counters(@owner.id, cached_counter_attribute_name => -records.size) if has_cached_counter?
+ def update_counter(difference, reflection = reflection)
+ if has_cached_counter?(reflection)
+ counter = cached_counter_attribute_name(reflection)
+ owner.class.update_counters(owner.id, counter => difference)
+ owner[counter] += difference
+ owner.changed_attributes.delete(counter) # eww
end
end
- def target_obsolete?
- false
+ # This shit is nasty. We need to avoid the following situation:
+ #
+ # * An associated record is deleted via record.destroy
+ # * Hence the callbacks run, and they find a belongs_to on the record with a
+ # :counter_cache options which points back at our owner. So they update the
+ # counter cache.
+ # * In which case, we must make sure to *not* update the counter cache, or else
+ # it will be decremented twice.
+ #
+ # Hence this method.
+ def inverse_updates_counter_cache?(reflection = reflection)
+ counter_name = cached_counter_attribute_name(reflection)
+ reflection.klass.reflect_on_all_associations(:belongs_to).any? { |inverse_reflection|
+ inverse_reflection.counter_cache_column == counter_name
+ }
end
- def construct_sql
- case
- when @reflection.options[:finder_sql]
- @finder_sql = interpolate_sql(@reflection.options[:finder_sql])
-
- when @reflection.options[:as]
- @finder_sql =
- "#{@reflection.quoted_table_name}.#{@reflection.options[:as]}_id = #{owner_quoted_id} AND " +
- "#{@reflection.quoted_table_name}.#{@reflection.options[:as]}_type = #{@owner.class.quote_value(@owner.class.base_class.name.to_s)}"
- @finder_sql << " AND (#{conditions})" if conditions
+ # Deletes the records according to the <tt>:dependent</tt> option.
+ def delete_records(records, method)
+ if method == :destroy
+ records.each { |r| r.destroy }
+ update_counter(-records.length) unless inverse_updates_counter_cache?
+ else
+ keys = records.map { |r| r[reflection.association_primary_key] }
+ scope = scoped.where(reflection.association_primary_key => keys)
+ if method == :delete_all
+ update_counter(-scope.delete_all)
else
- @finder_sql = "#{@reflection.quoted_table_name}.#{@reflection.primary_key_name} = #{owner_quoted_id}"
- @finder_sql << " AND (#{conditions})" if conditions
+ update_counter(-scope.update_all(reflection.foreign_key => nil))
+ end
end
-
- construct_counter_sql
- end
-
- def construct_scope
- create_scoping = {}
- set_belongs_to_association_for(create_scoping)
- {
- :find => { :conditions => @finder_sql,
- :readonly => false,
- :order => @reflection.options[:order],
- :limit => @reflection.options[:limit],
- :include => @reflection.options[:include]},
- :create => create_scoping
- }
- end
-
- def we_can_set_the_inverse_on_this?(record)
- inverse = @reflection.inverse_of
- return !inverse.nil?
end
end
end
diff --git a/activerecord/lib/active_record/associations/has_many_through_association.rb b/activerecord/lib/active_record/associations/has_many_through_association.rb
index 608b1c741a..9d2b29685b 100644
--- a/activerecord/lib/active_record/associations/has_many_through_association.rb
+++ b/activerecord/lib/active_record/associations/has_many_through_association.rb
@@ -1,114 +1,144 @@
-require "active_record/associations/through_association_scope"
require 'active_support/core_ext/object/blank'
module ActiveRecord
# = Active Record Has Many Through Association
module Associations
class HasManyThroughAssociation < HasManyAssociation #:nodoc:
- include ThroughAssociationScope
+ include ThroughAssociation
alias_method :new, :build
- def create!(attrs = nil)
- create_record(attrs, true)
- end
-
- def create(attrs = nil)
- create_record(attrs, false)
+ # Returns the size of the collection by executing a SELECT COUNT(*) query if the collection hasn't been
+ # loaded and calling collection.size if it has. If it's more likely than not that the collection does
+ # have a size larger than zero, and you need to fetch that collection afterwards, it'll take one fewer
+ # SELECT query if you use #length.
+ def size
+ if has_cached_counter?
+ owner.send(:read_attribute, cached_counter_attribute_name)
+ elsif loaded?
+ target.size
+ else
+ count
+ end
end
- def destroy(*records)
- transaction do
- delete_records(flatten_deeper(records))
- super
+ def concat(*records)
+ unless owner.new_record?
+ records.flatten.each do |record|
+ raise_on_type_mismatch(record)
+ record.save! if record.new_record?
+ end
end
+
+ super
end
- # Returns the size of the collection by executing a SELECT COUNT(*) query if the collection hasn't been
- # loaded and calling collection.size if it has. If it's more likely than not that the collection does
- # have a size larger than zero, and you need to fetch that collection afterwards, it'll take one fewer
- # SELECT query if you use #length.
- def size
- return @owner.send(:read_attribute, cached_counter_attribute_name) if has_cached_counter?
- return @target.size if loaded?
- return count
+ def insert_record(record, validate = true)
+ ensure_not_nested
+ return if record.new_record? && !record.save(:validate => validate)
+
+ through_record(record).save!
+ update_counter(1)
+ record
end
- protected
- def create_record(attrs, force = true)
- ensure_owner_is_not_new
+ private
- transaction do
- object = @reflection.klass.new(attrs)
- add_record_to_target_with_callbacks(object) {|r| insert_record(object, force) }
- object
+ def through_record(record)
+ through_association = owner.association(through_reflection.name)
+ attributes = construct_join_attributes(record)
+
+ through_record = Array.wrap(through_association.target).find { |candidate|
+ candidate.attributes.slice(*attributes.keys) == attributes
+ }
+
+ unless through_record
+ through_record = through_association.build(attributes)
+ through_record.send("#{source_reflection.name}=", record)
+ end
+
+ through_record
+ end
+
+ def build_record(attributes)
+ ensure_not_nested
+
+ record = super(attributes)
+
+ inverse = source_reflection.inverse_of
+ if inverse
+ if inverse.macro == :has_many
+ record.send(inverse.name) << through_record(record)
+ elsif inverse.macro == :has_one
+ record.send("#{inverse.name}=", through_record(record))
+ end
end
+
+ record
end
def target_reflection_has_associated_record?
- if @reflection.through_reflection.macro == :belongs_to && @owner[@reflection.through_reflection.primary_key_name].blank?
+ if through_reflection.macro == :belongs_to && owner[through_reflection.foreign_key].blank?
false
else
true
end
end
- def construct_find_options!(options)
- options[:joins] = construct_joins(options[:joins])
- options[:include] = @reflection.source_reflection.options[:include] if options[:include].nil? && @reflection.source_reflection.options[:include]
+ def update_through_counter?(method)
+ case method
+ when :destroy
+ !inverse_updates_counter_cache?(through_reflection)
+ when :nullify
+ false
+ else
+ true
+ end
end
- def insert_record(record, force = true, validate = true)
- if record.new_record?
- if force
- record.save!
- else
- return false unless record.save(:validate => validate)
- end
- end
+ def delete_records(records, method)
+ ensure_not_nested
- through_association = @owner.send(@reflection.through_reflection.name)
- through_record = through_association.create!(construct_join_attributes(record))
- through_association.proxy_target << through_record
- end
+ through = owner.association(through_reflection.name)
+ scope = through.scoped.where(construct_join_attributes(*records))
- # TODO - add dependent option support
- def delete_records(records)
- klass = @reflection.through_reflection.klass
- records.each do |associate|
- klass.delete_all(construct_join_attributes(associate))
+ case method
+ when :destroy
+ count = scope.destroy_all.length
+ when :nullify
+ count = scope.update_all(source_reflection.foreign_key => nil)
+ else
+ count = scope.delete_all
end
- end
- def find_target
- return [] unless target_reflection_has_associated_record?
- with_scope(construct_scope) { @reflection.klass.find(:all) }
- end
-
- def construct_sql
- case
- when @reflection.options[:finder_sql]
- @finder_sql = interpolate_sql(@reflection.options[:finder_sql])
+ delete_through_records(through, records)
- @finder_sql = "#{@reflection.quoted_table_name}.#{@reflection.primary_key_name} = #{owner_quoted_id}"
- @finder_sql << " AND (#{conditions})" if conditions
- else
- @finder_sql = construct_conditions
+ if through_reflection.macro == :has_many && update_through_counter?(method)
+ update_counter(-count, through_reflection)
end
- construct_counter_sql
+ update_counter(-count)
end
- def has_cached_counter?
- @owner.attribute_present?(cached_counter_attribute_name)
+ def delete_through_records(through, records)
+ if through_reflection.macro == :has_many
+ records.each do |record|
+ through.target.delete(through_record(record))
+ end
+ else
+ records.each do |record|
+ through.target = nil if through.target == through_record(record)
+ end
+ end
end
- def cached_counter_attribute_name
- "#{@reflection.name}_count"
+ def find_target
+ return [] unless target_reflection_has_associated_record?
+ scoped.all
end
# NOTE - not sure that we can actually cope with inverses here
- def we_can_set_the_inverse_on_this?(record)
+ def invertible_for?(record)
false
end
end
diff --git a/activerecord/lib/active_record/associations/has_one_association.rb b/activerecord/lib/active_record/associations/has_one_association.rb
index 68b8b792ad..1d2e8667e4 100644
--- a/activerecord/lib/active_record/associations/has_one_association.rb
+++ b/activerecord/lib/active_record/associations/has_one_association.rb
@@ -1,142 +1,70 @@
module ActiveRecord
# = Active Record Belongs To Has One Association
module Associations
- class HasOneAssociation < AssociationProxy #:nodoc:
- def initialize(owner, reflection)
- super
- construct_sql
- end
+ class HasOneAssociation < SingularAssociation #:nodoc:
+ def replace(record, save = true)
+ raise_on_type_mismatch(record) if record
+ load_target
- def create(attrs = {}, replace_existing = true)
- new_record(replace_existing) do |reflection|
- attrs = merge_with_conditions(attrs)
- reflection.create_association(attrs)
- end
- end
+ reflection.klass.transaction do
+ if target && target != record
+ remove_target!(options[:dependent])
+ end
- def create!(attrs = {}, replace_existing = true)
- new_record(replace_existing) do |reflection|
- attrs = merge_with_conditions(attrs)
- reflection.create_association!(attrs)
- end
- end
+ if record
+ set_inverse_instance(record)
+ set_owner_attributes(record)
- def build(attrs = {}, replace_existing = true)
- new_record(replace_existing) do |reflection|
- attrs = merge_with_conditions(attrs)
- reflection.build_association(attrs)
+ if owner.persisted? && save && !record.save
+ nullify_owner_attributes(record)
+ set_owner_attributes(target)
+ raise RecordNotSaved, "Failed to save the new associated #{reflection.name}."
+ end
+ end
end
- end
- def replace(obj, dont_save = false)
- load_target
+ self.target = record
+ end
- unless @target.nil? || @target == obj
- if dependent? && !dont_save
- case @reflection.options[:dependent]
+ def delete(method = options[:dependent])
+ if load_target
+ case method
when :delete
- @target.delete unless @target.new_record?
- @owner.clear_association_cache
+ target.delete
when :destroy
- @target.destroy unless @target.new_record?
- @owner.clear_association_cache
+ target.destroy
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?
+ target.update_attribute(reflection.foreign_key, nil)
end
end
-
- if obj.nil?
- @target = nil
- else
- raise_on_type_mismatch(obj)
- set_belongs_to_association_for(obj)
- @target = (AssociationProxy === obj ? obj.target : obj)
- end
-
- set_inverse_instance(obj, @owner)
- @loaded = true
-
- unless @owner.new_record? or obj.nil? or dont_save
- return (obj.save ? self : false)
- else
- return (obj.nil? ? nil : self)
- end
end
- protected
- def owner_quoted_id
- if @reflection.options[:primary_key]
- @owner.class.quote_value(@owner.send(@reflection.options[:primary_key]))
- else
- @owner.quoted_id
- end
- end
-
private
- def find_target
- the_target = @reflection.klass.find(:first,
- :conditions => @finder_sql,
- :select => @reflection.options[:select],
- :order => @reflection.options[:order],
- :include => @reflection.options[:include],
- :readonly => @reflection.options[:readonly]
- )
- set_inverse_instance(the_target, @owner)
- the_target
- end
- def construct_sql
- case
- when @reflection.options[:as]
- @finder_sql =
- "#{@reflection.quoted_table_name}.#{@reflection.options[:as]}_id = #{owner_quoted_id} AND " +
- "#{@reflection.quoted_table_name}.#{@reflection.options[:as]}_type = #{@owner.class.quote_value(@owner.class.base_class.name.to_s)}"
- else
- @finder_sql = "#{@reflection.quoted_table_name}.#{@reflection.primary_key_name} = #{owner_quoted_id}"
- end
- @finder_sql << " AND (#{conditions})" if conditions
+ # The reason that the save param for replace is false, if for create (not just build),
+ # is because the setting of the foreign keys is actually handled by the scoping when
+ # the record is instantiated, and so they are set straight away and do not need to be
+ # updated within replace.
+ def set_new_record(record)
+ replace(record, false)
end
- def construct_scope
- create_scoping = {}
- set_belongs_to_association_for(create_scoping)
- { :create => create_scoping }
- end
-
- def new_record(replace_existing)
- # Make sure we load the target first, if we plan on replacing the existing
- # instance. Otherwise, if the target has not previously been loaded
- # elsewhere, the instance we create will get orphaned.
- load_target if replace_existing
- record = @reflection.klass.send(:with_scope, :create => construct_scope[:create]) do
- yield @reflection
- end
-
- if replace_existing
- replace(record, true)
+ def remove_target!(method)
+ if [:delete, :destroy].include?(method)
+ target.send(method)
else
- record[@reflection.primary_key_name] = @owner.id unless @owner.new_record?
- self.target = record
- set_inverse_instance(record, @owner)
- end
+ nullify_owner_attributes(target)
- record
- end
-
- def we_can_set_the_inverse_on_this?(record)
- inverse = @reflection.inverse_of
- return !inverse.nil?
+ if target.persisted? && owner.persisted? && !target.save
+ set_owner_attributes(target)
+ raise RecordNotSaved, "Failed to remove the existing associated #{reflection.name}. " +
+ "The record failed to save when after its foreign key was set to nil."
+ end
+ end
end
- def merge_with_conditions(attrs={})
- attrs ||= {}
- attrs.update(@reflection.options[:conditions]) if @reflection.options[:conditions].is_a?(Hash)
- attrs
+ def nullify_owner_attributes(record)
+ record[reflection.foreign_key] = nil
end
end
end
diff --git a/activerecord/lib/active_record/associations/has_one_through_association.rb b/activerecord/lib/active_record/associations/has_one_through_association.rb
index fba0a2bfcc..fdf8ae1453 100644
--- a/activerecord/lib/active_record/associations/has_one_through_association.rb
+++ b/activerecord/lib/active_record/associations/has_one_through_association.rb
@@ -1,40 +1,36 @@
-require "active_record/associations/through_association_scope"
-
module ActiveRecord
# = Active Record Has One Through Association
module Associations
- class HasOneThroughAssociation < HasOneAssociation
- include ThroughAssociationScope
+ class HasOneThroughAssociation < HasOneAssociation #:nodoc:
+ include ThroughAssociation
- def replace(new_value)
- create_through_record(new_value)
- @target = new_value
+ def replace(record)
+ create_through_record(record)
+ self.target = record
end
private
- def create_through_record(new_value) #nodoc:
- klass = @reflection.through_reflection.klass
+ def create_through_record(record)
+ ensure_not_nested
+
+ through_proxy = owner.association(through_reflection.name)
+ through_record = through_proxy.send(:load_target)
- current_object = @owner.send(@reflection.through_reflection.name)
+ if through_record && !record
+ through_record.destroy
+ elsif record
+ attributes = construct_join_attributes(record)
- if current_object
- new_value ? current_object.update_attributes(construct_join_attributes(new_value)) : current_object.destroy
- elsif new_value
- if @owner.new_record?
- self.target = new_value
- through_association = @owner.send(:association_instance_get, @reflection.through_reflection.name)
- through_association.build(construct_join_attributes(new_value))
- else
- @owner.send(@reflection.through_reflection.name, klass.create(construct_join_attributes(new_value)))
+ if through_record
+ through_record.update_attributes(attributes)
+ elsif owner.new_record?
+ through_proxy.build(attributes)
+ else
+ through_proxy.create(attributes)
+ end
end
end
- end
-
- private
- def find_target
- with_scope(construct_scope) { @reflection.klass.find(:first) }
- end
end
end
end
diff --git a/activerecord/lib/active_record/associations/join_dependency.rb b/activerecord/lib/active_record/associations/join_dependency.rb
new file mode 100644
index 0000000000..504f25271c
--- /dev/null
+++ b/activerecord/lib/active_record/associations/join_dependency.rb
@@ -0,0 +1,215 @@
+module ActiveRecord
+ module Associations
+ class JoinDependency # :nodoc:
+ autoload :JoinPart, 'active_record/associations/join_dependency/join_part'
+ autoload :JoinBase, 'active_record/associations/join_dependency/join_base'
+ autoload :JoinAssociation, 'active_record/associations/join_dependency/join_association'
+
+ attr_reader :join_parts, :reflections, :alias_tracker, :active_record
+
+ def initialize(base, associations, joins)
+ @active_record = base
+ @table_joins = joins
+ @join_parts = [JoinBase.new(base)]
+ @associations = {}
+ @reflections = []
+ @alias_tracker = AliasTracker.new(joins)
+ @alias_tracker.aliased_name_for(base.table_name) # Updates the count for base.table_name to 1
+ build(associations)
+ end
+
+ def graft(*associations)
+ associations.each do |association|
+ join_associations.detect {|a| association == a} ||
+ build(association.reflection.name, association.find_parent_in(self) || join_base, association.join_type)
+ end
+ self
+ end
+
+ def join_associations
+ join_parts.last(join_parts.length - 1)
+ end
+
+ def join_base
+ join_parts.first
+ end
+
+ def columns
+ join_parts.collect { |join_part|
+ table = join_part.aliased_table
+ join_part.column_names_with_alias.collect{ |column_name, aliased_name|
+ table[column_name].as Arel.sql(aliased_name)
+ }
+ }.flatten
+ end
+
+ def instantiate(rows)
+ primary_key = join_base.aliased_primary_key
+ parents = {}
+
+ records = rows.map { |model|
+ primary_id = model[primary_key]
+ parent = parents[primary_id] ||= join_base.instantiate(model)
+ construct(parent, @associations, join_associations, model)
+ parent
+ }.uniq
+
+ remove_duplicate_results!(active_record, records, @associations)
+ records
+ end
+
+ def remove_duplicate_results!(base, records, associations)
+ case associations
+ when Symbol, String
+ reflection = base.reflections[associations]
+ remove_uniq_by_reflection(reflection, records)
+ when Array
+ associations.each do |association|
+ remove_duplicate_results!(base, records, association)
+ end
+ when Hash
+ associations.keys.each do |name|
+ reflection = base.reflections[name]
+ remove_uniq_by_reflection(reflection, records)
+
+ parent_records = []
+ records.each do |record|
+ if descendant = record.send(reflection.name)
+ if reflection.collection?
+ parent_records.concat descendant.target.uniq
+ else
+ parent_records << descendant
+ end
+ end
+ end
+
+ remove_duplicate_results!(reflection.klass, parent_records, associations[name]) unless parent_records.empty?
+ end
+ end
+ end
+
+ protected
+
+ def cache_joined_association(association)
+ associations = []
+ parent = association.parent
+ while parent != join_base
+ associations.unshift(parent.reflection.name)
+ parent = parent.parent
+ end
+ ref = @associations
+ associations.each do |key|
+ ref = ref[key]
+ end
+ ref[association.reflection.name] ||= {}
+ end
+
+ def build(associations, parent = nil, join_type = Arel::InnerJoin)
+ parent ||= join_parts.last
+ case associations
+ when Symbol, String
+ reflection = parent.reflections[associations.to_s.intern] or
+ raise ConfigurationError, "Association named '#{ associations }' was not found; perhaps you misspelled it?"
+ unless join_association = find_join_association(reflection, parent)
+ @reflections << reflection
+ join_association = build_join_association(reflection, parent)
+ join_association.join_type = join_type
+ @join_parts << join_association
+ cache_joined_association(join_association)
+ end
+ join_association
+ when Array
+ associations.each do |association|
+ build(association, parent, join_type)
+ end
+ when Hash
+ associations.keys.sort_by { |a| a.to_s }.each do |name|
+ join_association = build(name, parent, join_type)
+ build(associations[name], join_association, join_type)
+ end
+ else
+ raise ConfigurationError, associations.inspect
+ end
+ end
+
+ def find_join_association(name_or_reflection, parent)
+ if String === name_or_reflection
+ name_or_reflection = name_or_reflection.to_sym
+ end
+
+ join_associations.detect { |j|
+ j.reflection == name_or_reflection && j.parent == parent
+ }
+ end
+
+ def remove_uniq_by_reflection(reflection, records)
+ if reflection && reflection.collection?
+ records.each { |record| record.send(reflection.name).target.uniq! }
+ end
+ end
+
+ def build_join_association(reflection, parent)
+ JoinAssociation.new(reflection, self, parent)
+ end
+
+ def construct(parent, associations, join_parts, row)
+ case associations
+ when Symbol, String
+ name = associations.to_s
+
+ join_part = join_parts.detect { |j|
+ j.reflection.name.to_s == name &&
+ j.parent_table_name == parent.class.table_name }
+
+ raise(ConfigurationError, "No such association") unless join_part
+
+ join_parts.delete(join_part)
+ construct_association(parent, join_part, row)
+ when Array
+ associations.each do |association|
+ construct(parent, association, join_parts, row)
+ end
+ when Hash
+ associations.sort_by { |k,_| k.to_s }.each do |association_name, assoc|
+ association = construct(parent, association_name, join_parts, row)
+ construct(association, assoc, join_parts, row) if association
+ end
+ else
+ raise ConfigurationError, associations.inspect
+ end
+ end
+
+ def construct_association(record, join_part, row)
+ return if record.id.to_s != join_part.parent.record_id(row).to_s
+
+ macro = join_part.reflection.macro
+ if macro == :has_one
+ return if record.association_cache.key?(join_part.reflection.name)
+ association = join_part.instantiate(row) unless row[join_part.aliased_primary_key].nil?
+ set_target_and_inverse(join_part, association, record)
+ else
+ return if row[join_part.aliased_primary_key].nil?
+ association = join_part.instantiate(row)
+ case macro
+ when :has_many, :has_and_belongs_to_many
+ other = record.association(join_part.reflection.name)
+ other.loaded!
+ other.target.push(association)
+ other.set_inverse_instance(association)
+ when :belongs_to
+ set_target_and_inverse(join_part, association, record)
+ else
+ raise ConfigurationError, "unknown macro: #{join_part.reflection.macro}"
+ end
+ end
+ association
+ end
+
+ def set_target_and_inverse(join_part, association, record)
+ other = record.association(join_part.reflection.name)
+ other.target = association
+ other.set_inverse_instance(association)
+ end
+ end
+ end
+end
diff --git a/activerecord/lib/active_record/associations/join_dependency/join_association.rb b/activerecord/lib/active_record/associations/join_dependency/join_association.rb
new file mode 100644
index 0000000000..4121a5b378
--- /dev/null
+++ b/activerecord/lib/active_record/associations/join_dependency/join_association.rb
@@ -0,0 +1,144 @@
+module ActiveRecord
+ module Associations
+ class JoinDependency # :nodoc:
+ class JoinAssociation < JoinPart # :nodoc:
+ include JoinHelper
+
+ # The reflection of the association represented
+ attr_reader :reflection
+
+ # The JoinDependency object which this JoinAssociation exists within. This is mainly
+ # relevant for generating aliases which do not conflict with other joins which are
+ # part of the query.
+ attr_reader :join_dependency
+
+ # A JoinBase instance representing the active record we are joining onto.
+ # (So in Author.has_many :posts, the Author would be that base record.)
+ attr_reader :parent
+
+ # What type of join will be generated, either Arel::InnerJoin (default) or Arel::OuterJoin
+ attr_accessor :join_type
+
+ # These implement abstract methods from the superclass
+ attr_reader :aliased_prefix
+
+ attr_reader :tables
+
+ delegate :options, :through_reflection, :source_reflection, :chain, :to => :reflection
+ delegate :table, :table_name, :to => :parent, :prefix => :parent
+ delegate :alias_tracker, :to => :join_dependency
+
+ alias :alias_suffix :parent_table_name
+
+ def initialize(reflection, join_dependency, parent = nil)
+ reflection.check_validity!
+
+ if reflection.options[:polymorphic]
+ raise EagerLoadPolymorphicError.new(reflection)
+ end
+
+ super(reflection.klass)
+
+ @reflection = reflection
+ @join_dependency = join_dependency
+ @parent = parent
+ @join_type = Arel::InnerJoin
+ @aliased_prefix = "t#{ join_dependency.join_parts.size }"
+ @tables = construct_tables.reverse
+ end
+
+ def ==(other)
+ other.class == self.class &&
+ other.reflection == reflection &&
+ other.parent == parent
+ end
+
+ def find_parent_in(other_join_dependency)
+ other_join_dependency.join_parts.detect do |join_part|
+ parent == join_part
+ end
+ end
+
+ def join_to(relation)
+ tables = @tables.dup
+ foreign_table = parent_table
+
+ # The chain starts with the target table, but we want to end with it here (makes
+ # more sense in this context), so we reverse
+ chain.reverse.each_with_index do |reflection, i|
+ table = tables.shift
+
+ case reflection.source_macro
+ when :belongs_to
+ key = reflection.association_primary_key
+ foreign_key = reflection.foreign_key
+ when :has_and_belongs_to_many
+ # Join the join table first...
+ relation.from(join(
+ table,
+ table[reflection.foreign_key].
+ eq(foreign_table[reflection.active_record_primary_key])
+ ))
+
+ foreign_table, table = table, tables.shift
+
+ key = reflection.association_primary_key
+ foreign_key = reflection.association_foreign_key
+ else
+ key = reflection.foreign_key
+ foreign_key = reflection.active_record_primary_key
+ end
+
+ constraint = table[key].eq(foreign_table[foreign_key])
+
+ if reflection.klass.finder_needs_type_condition?
+ constraint = table.create_and([
+ constraint,
+ reflection.klass.send(:type_condition, table)
+ ])
+ end
+
+ relation.from(join(table, constraint))
+
+ unless conditions[i].empty?
+ relation.where(sanitize(conditions[i], table))
+ end
+
+ # The current table in this iteration becomes the foreign table in the next
+ foreign_table = table
+ end
+
+ relation
+ end
+
+ def join_relation(joining_relation)
+ self.join_type = Arel::OuterJoin
+ joining_relation.joins(self)
+ end
+
+ def table
+ tables.last
+ end
+
+ def aliased_table_name
+ table.table_alias || table.name
+ end
+
+ def conditions
+ @conditions ||= reflection.conditions.reverse
+ end
+
+ private
+
+ def interpolate(conditions)
+ if conditions.respond_to?(:to_proc)
+ instance_eval(&conditions)
+ else
+ conditions
+ end
+ end
+
+ end
+ end
+ end
+end
diff --git a/activerecord/lib/active_record/associations/join_dependency/join_base.rb b/activerecord/lib/active_record/associations/join_dependency/join_base.rb
new file mode 100644
index 0000000000..3920e84976
--- /dev/null
+++ b/activerecord/lib/active_record/associations/join_dependency/join_base.rb
@@ -0,0 +1,24 @@
+module ActiveRecord
+ module Associations
+ class JoinDependency # :nodoc:
+ class JoinBase < JoinPart # :nodoc:
+ def ==(other)
+ other.class == self.class &&
+ other.active_record == active_record
+ end
+
+ def aliased_prefix
+ "t0"
+ end
+
+ def table
+ Arel::Table.new(table_name, arel_engine)
+ end
+
+ def aliased_table_name
+ active_record.table_name
+ end
+ end
+ end
+ end
+end
diff --git a/activerecord/lib/active_record/associations/join_dependency/join_part.rb b/activerecord/lib/active_record/associations/join_dependency/join_part.rb
new file mode 100644
index 0000000000..3279e56e7d
--- /dev/null
+++ b/activerecord/lib/active_record/associations/join_dependency/join_part.rb
@@ -0,0 +1,78 @@
+module ActiveRecord
+ module Associations
+ class JoinDependency # :nodoc:
+ # A JoinPart represents a part of a JoinDependency. It is an abstract class, inherited
+ # by JoinBase and JoinAssociation. A JoinBase represents the Active Record which
+ # everything else is being joined onto. A JoinAssociation represents an association which
+ # is joining to the base. A JoinAssociation may result in more than one actual join
+ # operations (for example a has_and_belongs_to_many JoinAssociation would result in
+ # two; one for the join table and one for the target table).
+ class JoinPart # :nodoc:
+ # The Active Record class which this join part is associated 'about'; for a JoinBase
+ # this is the actual base model, for a JoinAssociation this is the target model of the
+ # association.
+ attr_reader :active_record
+
+ delegate :table_name, :column_names, :primary_key, :reflections, :arel_engine, :to => :active_record
+
+ def initialize(active_record)
+ @active_record = active_record
+ @cached_record = {}
+ @column_names_with_alias = nil
+ end
+
+ def aliased_table
+ Arel::Nodes::TableAlias.new aliased_table_name, table
+ end
+
+ def ==(other)
+ raise NotImplementedError
+ end
+
+ # An Arel::Table for the active_record
+ def table
+ raise NotImplementedError
+ end
+
+ # The prefix to be used when aliasing columns in the active_record's table
+ def aliased_prefix
+ raise NotImplementedError
+ end
+
+ # The alias for the active_record's table
+ def aliased_table_name
+ raise NotImplementedError
+ end
+
+ # The alias for the primary key of the active_record's table
+ def aliased_primary_key
+ "#{aliased_prefix}_r0"
+ end
+
+ # An array of [column_name, alias] pairs for the table
+ def column_names_with_alias
+ unless @column_names_with_alias
+ @column_names_with_alias = []
+
+ ([primary_key] + (column_names - [primary_key])).each_with_index do |column_name, i|
+ @column_names_with_alias << [column_name, "#{aliased_prefix}_r#{i}"]
+ end
+ end
+ @column_names_with_alias
+ end
+
+ def extract_record(row)
+ Hash[column_names_with_alias.map{|cn, an| [cn, row[an]]}]
+ end
+
+ def record_id(row)
+ row[aliased_primary_key]
+ end
+
+ def instantiate(row)
+ @cached_record[record_id(row)] ||= active_record.send(:instantiate, extract_record(row))
+ end
+ end
+ end
+ end
+end
diff --git a/activerecord/lib/active_record/associations/join_helper.rb b/activerecord/lib/active_record/associations/join_helper.rb
new file mode 100644
index 0000000000..eae546e76e
--- /dev/null
+++ b/activerecord/lib/active_record/associations/join_helper.rb
@@ -0,0 +1,56 @@
+module ActiveRecord
+ module Associations
+ # Helper class module which gets mixed into JoinDependency::JoinAssociation and AssociationScope
+ module JoinHelper #:nodoc:
+
+ def join_type
+ Arel::InnerJoin
+ end
+
+ private
+
+ def construct_tables
+ tables = []
+ chain.each do |reflection|
+ tables << alias_tracker.aliased_table_for(
+ table_name_for(reflection),
+ table_alias_for(reflection, reflection != self.reflection)
+ )
+
+ if reflection.source_macro == :has_and_belongs_to_many
+ tables << alias_tracker.aliased_table_for(
+ (reflection.source_reflection || reflection).options[:join_table],
+ table_alias_for(reflection, true)
+ )
+ end
+ end
+ tables
+ end
+
+ def table_name_for(reflection)
+ reflection.table_name
+ end
+
+ def table_alias_for(reflection, join = false)
+ name = alias_tracker.pluralize(reflection.name)
+ name << "_#{alias_suffix}"
+ name << "_join" if join
+ name
+ end
+
+ def join(table, constraint)
+ table.create_join(table, table.create_on(constraint), join_type)
+ end
+
+ def sanitize(conditions, table)
+ conditions = conditions.map do |condition|
+ condition = active_record.send(:sanitize_sql, interpolate(condition), table.table_alias || table.name)
+ condition = Arel.sql(condition) unless condition.is_a?(Arel::Node)
+ condition
+ end
+
+ conditions.length == 1 ? conditions.first : Arel::Nodes::And.new(conditions)
+ end
+ end
+ end
+end
diff --git a/activerecord/lib/active_record/associations/preloader.rb b/activerecord/lib/active_record/associations/preloader.rb
new file mode 100644
index 0000000000..fafed94ff2
--- /dev/null
+++ b/activerecord/lib/active_record/associations/preloader.rb
@@ -0,0 +1,177 @@
+module ActiveRecord
+ module Associations
+ # Implements the details of eager loading of Active Record associations.
+ #
+ # Note that 'eager loading' and 'preloading' are actually the same thing.
+ # However, there are two different eager loading strategies.
+ #
+ # The first one is by using table joins. This was only strategy available
+ # prior to Rails 2.1. Suppose that you have an Author model with columns
+ # 'name' and 'age', and a Book model with columns 'name' and 'sales'. Using
+ # this strategy, Active Record would try to retrieve all data for an author
+ # and all of its books via a single query:
+ #
+ # SELECT * FROM authors
+ # LEFT OUTER JOIN books ON authors.id = books.id
+ # WHERE authors.name = 'Ken Akamatsu'
+ #
+ # However, this could result in many rows that contain redundant data. After
+ # having received the first row, we already have enough data to instantiate
+ # the Author object. In all subsequent rows, only the data for the joined
+ # 'books' table is useful; the joined 'authors' data is just redundant, and
+ # processing this redundant data takes memory and CPU time. The problem
+ # quickly becomes worse and worse as the level of eager loading increases
+ # (i.e. if Active Record is to eager load the associations' associations as
+ # well).
+ #
+ # The second strategy is to use multiple database queries, one for each
+ # level of association. Since Rails 2.1, this is the default strategy. In
+ # situations where a table join is necessary (e.g. when the +:conditions+
+ # option references an association's column), it will fallback to the table
+ # join strategy.
+ class Preloader #:nodoc:
+ autoload :Association, 'active_record/associations/preloader/association'
+ autoload :SingularAssociation, 'active_record/associations/preloader/singular_association'
+ autoload :CollectionAssociation, 'active_record/associations/preloader/collection_association'
+ autoload :ThroughAssociation, 'active_record/associations/preloader/through_association'
+
+ autoload :HasMany, 'active_record/associations/preloader/has_many'
+ autoload :HasManyThrough, 'active_record/associations/preloader/has_many_through'
+ autoload :HasOne, 'active_record/associations/preloader/has_one'
+ autoload :HasOneThrough, 'active_record/associations/preloader/has_one_through'
+ autoload :HasAndBelongsToMany, 'active_record/associations/preloader/has_and_belongs_to_many'
+ autoload :BelongsTo, 'active_record/associations/preloader/belongs_to'
+
+ attr_reader :records, :associations, :options, :model
+
+ # Eager loads the named associations for the given Active Record record(s).
+ #
+ # In this description, 'association name' shall refer to the name passed
+ # to an association creation method. For example, a model that specifies
+ # <tt>belongs_to :author</tt>, <tt>has_many :buyers</tt> has association
+ # names +:author+ and +:buyers+.
+ #
+ # == Parameters
+ # +records+ is an array of ActiveRecord::Base. This array needs not be flat,
+ # i.e. +records+ itself may also contain arrays of records. In any case,
+ # +preload_associations+ will preload the all associations records by
+ # flattening +records+.
+ #
+ # +associations+ specifies one or more associations that you want to
+ # preload. It may be:
+ # - a Symbol or a String which specifies a single association name. For
+ # example, specifying +:books+ allows this method to preload all books
+ # for an Author.
+ # - an Array which specifies multiple association names. This array
+ # is processed recursively. For example, specifying <tt>[:avatar, :books]</tt>
+ # allows this method to preload an author's avatar as well as all of his
+ # books.
+ # - a Hash which specifies multiple association names, as well as
+ # association names for the to-be-preloaded association objects. For
+ # example, specifying <tt>{ :author => :avatar }</tt> will preload a
+ # book's author, as well as that author's avatar.
+ #
+ # +:associations+ has the same format as the +:include+ option for
+ # <tt>ActiveRecord::Base.find</tt>. So +associations+ could look like this:
+ #
+ # :books
+ # [ :books, :author ]
+ # { :author => :avatar }
+ # [ :books, { :author => :avatar } ]
+ #
+ # +options+ contains options that will be passed to ActiveRecord::Base#find
+ # (which is called under the hood for preloading records). But it is passed
+ # only one level deep in the +associations+ argument, i.e. it's not passed
+ # to the child associations when +associations+ is a Hash.
+ def initialize(records, associations, options = {})
+ @records = Array.wrap(records).compact.uniq
+ @associations = Array.wrap(associations)
+ @options = options
+ end
+
+ def run
+ unless records.empty?
+ associations.each { |association| preload(association) }
+ end
+ end
+
+ private
+
+ def preload(association)
+ case association
+ when Hash
+ preload_hash(association)
+ when String, Symbol
+ preload_one(association.to_sym)
+ else
+ raise ArgumentError, "#{association.inspect} was not recognised for preload"
+ end
+ end
+
+ def preload_hash(association)
+ association.each do |parent, child|
+ Preloader.new(records, parent, options).run
+ Preloader.new(records.map { |record| record.send(parent) }.flatten, child).run
+ end
+ end
+
+ # Not all records have the same class, so group then preload group on the reflection
+ # itself so that if various subclass share the same association then we do not split
+ # them unnecessarily
+ #
+ # Additionally, polymorphic belongs_to associations can have multiple associated
+ # classes, depending on the polymorphic_type field. So we group by the classes as
+ # well.
+ def preload_one(association)
+ grouped_records(association).each do |reflection, klasses|
+ klasses.each do |klass, records|
+ preloader_for(reflection).new(klass, records, reflection, options).run
+ end
+ end
+ end
+
+ def grouped_records(association)
+ Hash[
+ records_by_reflection(association).map do |reflection, records|
+ [reflection, records.group_by { |record| association_klass(reflection, record) }]
+ end
+ ]
+ end
+
+ def records_by_reflection(association)
+ records.group_by do |record|
+ reflection = record.class.reflections[association]
+
+ unless reflection
+ raise ActiveRecord::ConfigurationError, "Association named '#{association}' was not found; " \
+ "perhaps you misspelled it?"
+ end
+
+ reflection
+ end
+ end
+
+ def association_klass(reflection, record)
+ if reflection.macro == :belongs_to && reflection.options[:polymorphic]
+ klass = record.send(reflection.foreign_type)
+ klass && klass.constantize
+ else
+ reflection.klass
+ end
+ end
+
+ def preloader_for(reflection)
+ case reflection.macro
+ when :has_many
+ reflection.options[:through] ? HasManyThrough : HasMany
+ when :has_one
+ reflection.options[:through] ? HasOneThrough : HasOne
+ when :has_and_belongs_to_many
+ HasAndBelongsToMany
+ when :belongs_to
+ BelongsTo
+ end
+ end
+ end
+ end
+end
diff --git a/activerecord/lib/active_record/associations/preloader/association.rb b/activerecord/lib/active_record/associations/preloader/association.rb
new file mode 100644
index 0000000000..7256dd5288
--- /dev/null
+++ b/activerecord/lib/active_record/associations/preloader/association.rb
@@ -0,0 +1,126 @@
+module ActiveRecord
+ module Associations
+ class Preloader
+ class Association #:nodoc:
+ attr_reader :owners, :reflection, :preload_options, :model, :klass
+
+ def initialize(klass, owners, reflection, preload_options)
+ @klass = klass
+ @owners = owners
+ @reflection = reflection
+ @preload_options = preload_options || {}
+ @model = owners.first && owners.first.class
+ @scoped = nil
+ @owners_by_key = nil
+ end
+
+ def run
+ unless owners.first.association(reflection.name).loaded?
+ preload
+ end
+ end
+
+ def preload
+ raise NotImplementedError
+ end
+
+ def scoped
+ @scoped ||= build_scope
+ end
+
+ def records_for(ids)
+ scoped.where(association_key.in(ids))
+ end
+
+ def table
+ klass.arel_table
+ end
+
+ # The name of the key on the associated records
+ def association_key_name
+ raise NotImplementedError
+ end
+
+ # This is overridden by HABTM as the condition should be on the foreign_key column in
+ # the join table
+ def association_key
+ table[association_key_name]
+ end
+
+ # The name of the key on the model which declares the association
+ def owner_key_name
+ raise NotImplementedError
+ end
+
+ # We're converting to a string here because postgres will return the aliased association
+ # key in a habtm as a string (for whatever reason)
+ def owners_by_key
+ @owners_by_key ||= owners.group_by do |owner|
+ key = owner[owner_key_name]
+ key && key.to_s
+ end
+ end
+
+ def options
+ reflection.options
+ end
+
+ private
+
+ def associated_records_by_owner
+ owner_keys = owners.map { |owner| owner[owner_key_name] }.compact.uniq
+
+ if klass.nil? || owner_keys.empty?
+ records = []
+ else
+ # Some databases impose a limit on the number of ids in a list (in Oracle it's 1000)
+ # Make several smaller queries if necessary or make one query if the adapter supports it
+ sliced = owner_keys.each_slice(model.connection.in_clause_length || owner_keys.size)
+ records = sliced.map { |slice| records_for(slice) }.flatten
+ end
+
+ # Each record may have multiple owners, and vice-versa
+ records_by_owner = Hash[owners.map { |owner| [owner, []] }]
+ records.each do |record|
+ owner_key = record[association_key_name].to_s
+
+ owners_by_key[owner_key].each do |owner|
+ records_by_owner[owner] << record
+ end
+ end
+ records_by_owner
+ end
+
+ def build_scope
+ scope = klass.scoped
+
+ scope = scope.where(process_conditions(options[:conditions]))
+ scope = scope.where(process_conditions(preload_options[:conditions]))
+
+ scope = scope.select(preload_options[:select] || options[:select] || table[Arel.star])
+ scope = scope.includes(preload_options[:include] || options[:include])
+
+ if options[:as]
+ scope = scope.where(
+ klass.table_name => {
+ reflection.type => model.base_class.sti_name
+ }
+ )
+ end
+
+ scope
+ end
+
+ def process_conditions(conditions)
+ if conditions.respond_to?(:to_proc)
+ conditions = klass.send(:instance_eval, &conditions)
+ end
+
+ if conditions
+ klass.send(:sanitize_sql, conditions)
+ end
+ end
+ end
+ end
+ end
+end
diff --git a/activerecord/lib/active_record/associations/preloader/belongs_to.rb b/activerecord/lib/active_record/associations/preloader/belongs_to.rb
new file mode 100644
index 0000000000..5091d4717a
--- /dev/null
+++ b/activerecord/lib/active_record/associations/preloader/belongs_to.rb
@@ -0,0 +1,17 @@
+module ActiveRecord
+ module Associations
+ class Preloader
+ class BelongsTo < SingularAssociation #:nodoc:
+
+ def association_key_name
+ reflection.options[:primary_key] || klass && klass.primary_key
+ end
+
+ def owner_key_name
+ reflection.foreign_key
+ end
+
+ end
+ end
+ end
+end
diff --git a/activerecord/lib/active_record/associations/preloader/collection_association.rb b/activerecord/lib/active_record/associations/preloader/collection_association.rb
new file mode 100644
index 0000000000..c248aeaaf6
--- /dev/null
+++ b/activerecord/lib/active_record/associations/preloader/collection_association.rb
@@ -0,0 +1,24 @@
+module ActiveRecord
+ module Associations
+ class Preloader
+ class CollectionAssociation < Association #:nodoc:
+
+ private
+
+ def build_scope
+ super.order(preload_options[:order] || options[:order])
+ end
+
+ def preload
+ associated_records_by_owner.each do |owner, records|
+ association = owner.association(reflection.name)
+ association.loaded!
+ association.target.concat(records)
+ records.each { |record| association.set_inverse_instance(record) }
+ end
+ end
+
+ end
+ end
+ end
+end
diff --git a/activerecord/lib/active_record/associations/preloader/has_and_belongs_to_many.rb b/activerecord/lib/active_record/associations/preloader/has_and_belongs_to_many.rb
new file mode 100644
index 0000000000..24be279449
--- /dev/null
+++ b/activerecord/lib/active_record/associations/preloader/has_and_belongs_to_many.rb
@@ -0,0 +1,60 @@
+module ActiveRecord
+ module Associations
+ class Preloader
+ class HasAndBelongsToMany < CollectionAssociation #:nodoc:
+ attr_reader :join_table
+
+ def initialize(klass, records, reflection, preload_options)
+ super
+ @join_table = Arel::Table.new(options[:join_table]).alias('t0')
+ end
+
+ # Unlike the other associations, we want to get a raw array of rows so that we can
+ # access the aliased column on the join table
+ def records_for(ids)
+ scope = super
+ klass.connection.select_all(scope.arel.to_sql, 'SQL', scope.bind_values)
+ end
+
+ def owner_key_name
+ reflection.active_record_primary_key
+ end
+
+ def association_key_name
+ 'ar_association_key_name'
+ end
+
+ def association_key
+ join_table[reflection.foreign_key]
+ end
+
+ private
+
+ # Once we have used the join table column (in super), we manually instantiate the
+ # actual records, ensuring that we don't create more than one instances of the same
+ # record
+ def associated_records_by_owner
+ records = {}
+ super.each do |owner_key, rows|
+ rows.map! { |row| records[row[klass.primary_key]] ||= klass.instantiate(row) }
+ end
+ end
+
+ def build_scope
+ super.joins(join).select(join_select)
+ end
+
+ def join_select
+ association_key.as(Arel.sql(association_key_name))
+ end
+
+ def join
+ condition = table[reflection.association_primary_key].eq(
+ join_table[reflection.association_foreign_key])
+
+ table.create_join(join_table, table.create_on(condition))
+ end
+ end
+ end
+ end
+end
diff --git a/activerecord/lib/active_record/associations/preloader/has_many.rb b/activerecord/lib/active_record/associations/preloader/has_many.rb
new file mode 100644
index 0000000000..3ea91a8c11
--- /dev/null
+++ b/activerecord/lib/active_record/associations/preloader/has_many.rb
@@ -0,0 +1,17 @@
+module ActiveRecord
+ module Associations
+ class Preloader
+ class HasMany < CollectionAssociation #:nodoc:
+
+ def association_key_name
+ reflection.foreign_key
+ end
+
+ def owner_key_name
+ reflection.active_record_primary_key
+ end
+
+ end
+ end
+ end
+end
diff --git a/activerecord/lib/active_record/associations/preloader/has_many_through.rb b/activerecord/lib/active_record/associations/preloader/has_many_through.rb
new file mode 100644
index 0000000000..c6e9ede356
--- /dev/null
+++ b/activerecord/lib/active_record/associations/preloader/has_many_through.rb
@@ -0,0 +1,15 @@
+module ActiveRecord
+ module Associations
+ class Preloader
+ class HasManyThrough < CollectionAssociation #:nodoc:
+ include ThroughAssociation
+
+ def associated_records_by_owner
+ super.each do |owner, records|
+ records.uniq! if options[:uniq]
+ end
+ end
+ end
+ end
+ end
+end
diff --git a/activerecord/lib/active_record/associations/preloader/has_one.rb b/activerecord/lib/active_record/associations/preloader/has_one.rb
new file mode 100644
index 0000000000..848448bb48
--- /dev/null
+++ b/activerecord/lib/active_record/associations/preloader/has_one.rb
@@ -0,0 +1,23 @@
+module ActiveRecord
+ module Associations
+ class Preloader
+ class HasOne < SingularAssociation #:nodoc:
+
+ def association_key_name
+ reflection.foreign_key
+ end
+
+ def owner_key_name
+ reflection.active_record_primary_key
+ end
+
+ private
+
+ def build_scope
+ super.order(preload_options[:order] || options[:order])
+ end
+
+ end
+ end
+ end
+end
diff --git a/activerecord/lib/active_record/associations/preloader/has_one_through.rb b/activerecord/lib/active_record/associations/preloader/has_one_through.rb
new file mode 100644
index 0000000000..f063f85574
--- /dev/null
+++ b/activerecord/lib/active_record/associations/preloader/has_one_through.rb
@@ -0,0 +1,9 @@
+module ActiveRecord
+ module Associations
+ class Preloader
+ class HasOneThrough < SingularAssociation #:nodoc:
+ include ThroughAssociation
+ end
+ end
+ end
+end
diff --git a/activerecord/lib/active_record/associations/preloader/singular_association.rb b/activerecord/lib/active_record/associations/preloader/singular_association.rb
new file mode 100644
index 0000000000..44e804d785
--- /dev/null
+++ b/activerecord/lib/active_record/associations/preloader/singular_association.rb
@@ -0,0 +1,21 @@
+module ActiveRecord
+ module Associations
+ class Preloader
+ class SingularAssociation < Association #:nodoc:
+
+ private
+
+ def preload
+ associated_records_by_owner.each do |owner, associated_records|
+ record = associated_records.first
+
+ association = owner.association(reflection.name)
+ association.target = record
+ association.set_inverse_instance(record)
+ end
+ end
+
+ end
+ end
+ end
+end
diff --git a/activerecord/lib/active_record/associations/preloader/through_association.rb b/activerecord/lib/active_record/associations/preloader/through_association.rb
new file mode 100644
index 0000000000..ad6374d09a
--- /dev/null
+++ b/activerecord/lib/active_record/associations/preloader/through_association.rb
@@ -0,0 +1,67 @@
+module ActiveRecord
+ module Associations
+ class Preloader
+ module ThroughAssociation #:nodoc:
+
+ def through_reflection
+ reflection.through_reflection
+ end
+
+ def source_reflection
+ reflection.source_reflection
+ end
+
+ def associated_records_by_owner
+ through_records = through_records_by_owner
+
+ ActiveRecord::Associations::Preloader.new(
+ through_records.values.flatten,
+ source_reflection.name, options
+ ).run
+
+ through_records.each do |owner, records|
+ records.map! { |r| r.send(source_reflection.name) }.flatten!
+ records.compact!
+ end
+ end
+
+ private
+
+ def through_records_by_owner
+ ActiveRecord::Associations::Preloader.new(
+ owners, through_reflection.name,
+ through_options
+ ).run
+
+ Hash[owners.map do |owner|
+ through_records = Array.wrap(owner.send(through_reflection.name))
+
+ # Dont cache the association - we would only be caching a subset
+ if reflection.options[:source_type] && through_reflection.collection?
+ owner.association(through_reflection.name).reset
+ end
+
+ [owner, through_records]
+ end]
+ end
+
+ def through_options
+ through_options = {}
+
+ if options[:source_type]
+ through_options[:conditions] = { reflection.foreign_type => options[:source_type] }
+ else
+ if options[:conditions]
+ through_options[:include] = options[:include] || options[:source]
+ through_options[:conditions] = options[:conditions]
+ end
+
+ through_options[:order] = options[:order]
+ end
+
+ through_options
+ end
+ end
+ end
+ end
+end
diff --git a/activerecord/lib/active_record/associations/singular_association.rb b/activerecord/lib/active_record/associations/singular_association.rb
new file mode 100644
index 0000000000..4edbe216be
--- /dev/null
+++ b/activerecord/lib/active_record/associations/singular_association.rb
@@ -0,0 +1,55 @@
+module ActiveRecord
+ module Associations
+ class SingularAssociation < Association #:nodoc:
+ # Implements the reader method, e.g. foo.bar for Foo.has_one :bar
+ def reader(force_reload = false)
+ if force_reload
+ klass.uncached { reload }
+ elsif !loaded? || stale_target?
+ reload
+ end
+
+ target
+ end
+
+ # Implements the writer method, e.g. foo.items= for Foo.has_many :items
+ def writer(record)
+ replace(record)
+ end
+
+ def create(attributes = {})
+ new_record(:create, attributes)
+ end
+
+ def create!(attributes = {})
+ build(attributes).tap { |record| record.save! }
+ end
+
+ def build(attributes = {})
+ new_record(:build, attributes)
+ end
+
+ private
+
+ def find_target
+ scoped.first.tap { |record| set_inverse_instance(record) }
+ end
+
+ # Implemented by subclasses
+ def replace(record)
+ raise NotImplementedError, "Subclasses must implement a replace(record) method"
+ end
+
+ def set_new_record(record)
+ replace(record)
+ end
+
+ def new_record(method, attributes)
+ attributes = scoped.scope_for_create.merge(attributes || {})
+ record = reflection.send("#{method}_association", attributes)
+ set_new_record(record)
+ record
+ end
+ end
+ end
+end
diff --git a/activerecord/lib/active_record/associations/through_association.rb b/activerecord/lib/active_record/associations/through_association.rb
new file mode 100644
index 0000000000..e6ab628719
--- /dev/null
+++ b/activerecord/lib/active_record/associations/through_association.rb
@@ -0,0 +1,80 @@
+module ActiveRecord
+ # = Active Record Through Association
+ module Associations
+ module ThroughAssociation #:nodoc:
+
+ delegate :source_reflection, :through_reflection, :chain, :to => :reflection
+
+ protected
+
+ # We merge in these scopes for two reasons:
+ #
+ # 1. To get the default_scope conditions for any of the other reflections in the chain
+ # 2. To get the type conditions for any STI models in the chain
+ def target_scope
+ scope = super
+ chain[1..-1].each do |reflection|
+ scope = scope.merge(reflection.klass.scoped)
+ end
+ scope
+ end
+
+ private
+
+ # Construct attributes for :through pointing to owner and associate. This is used by the
+ # methods which create and delete records on the association.
+ #
+ # We only support indirectly modifying through associations which has a belongs_to source.
+ # This is the "has_many :tags, :through => :taggings" situation, where the join model
+ # typically has a belongs_to on both side. In other words, associations which could also
+ # be represented as has_and_belongs_to_many associations.
+ #
+ # We do not support creating/deleting records on the association where the source has
+ # some other type, because this opens up a whole can of worms, and in basically any
+ # situation it is more natural for the user to just create or modify their join records
+ # directly as required.
+ def construct_join_attributes(*records)
+ if source_reflection.macro != :belongs_to
+ raise HasManyThroughCantAssociateThroughHasOneOrManyReflection.new(owner, reflection)
+ end
+
+ join_attributes = {
+ source_reflection.foreign_key =>
+ records.map { |record|
+ record.send(source_reflection.association_primary_key)
+ }
+ }
+
+ if options[:source_type]
+ join_attributes[source_reflection.foreign_type] =
+ records.map { |record| record.class.base_class.name }
+ end
+
+ if records.count == 1
+ Hash[join_attributes.map { |k, v| [k, v.first] }]
+ else
+ join_attributes
+ end
+ end
+
+ # Note: this does not capture all cases, for example it would be crazy to try to
+ # properly support stale-checking for nested associations.
+ def stale_state
+ if through_reflection.macro == :belongs_to
+ owner[through_reflection.foreign_key].to_s
+ end
+ end
+
+ def foreign_key_present?
+ through_reflection.macro == :belongs_to &&
+ !owner[through_reflection.foreign_key].nil?
+ end
+
+ def ensure_not_nested
+ if reflection.nested?
+ raise HasManyThroughNestedAssociationsAreReadonly.new(owner, reflection)
+ end
+ end
+ end
+ end
+end
diff --git a/activerecord/lib/active_record/associations/through_association_scope.rb b/activerecord/lib/active_record/associations/through_association_scope.rb
deleted file mode 100644
index cabb33c4a8..0000000000
--- a/activerecord/lib/active_record/associations/through_association_scope.rb
+++ /dev/null
@@ -1,154 +0,0 @@
-module ActiveRecord
- # = Active Record Through Association Scope
- module Associations
- module ThroughAssociationScope
-
- protected
-
- def construct_scope
- { :create => construct_owner_attributes(@reflection),
- :find => { :conditions => construct_conditions,
- :joins => construct_joins,
- :include => @reflection.options[:include] || @reflection.source_reflection.options[:include],
- :select => construct_select,
- :order => @reflection.options[:order],
- :limit => @reflection.options[:limit],
- :readonly => @reflection.options[:readonly],
- } }
- end
-
- # Build SQL conditions from attributes, qualified by table name.
- def construct_conditions
- table_name = @reflection.through_reflection.quoted_table_name
- conditions = construct_quoted_owner_attributes(@reflection.through_reflection).map do |attr, value|
- "#{table_name}.#{attr} = #{value}"
- end
- conditions << sql_conditions if sql_conditions
- "(" + conditions.join(') AND (') + ")"
- end
-
- # Associate attributes pointing to owner, quoted.
- def construct_quoted_owner_attributes(reflection)
- if as = reflection.options[:as]
- { "#{as}_id" => owner_quoted_id,
- "#{as}_type" => reflection.klass.quote_value(
- @owner.class.base_class.name.to_s,
- reflection.klass.columns_hash["#{as}_type"]) }
- elsif reflection.macro == :belongs_to
- { reflection.klass.primary_key => @owner.class.quote_value(@owner[reflection.primary_key_name]) }
- else
- { reflection.primary_key_name => owner_quoted_id }
- end
- end
-
- def construct_from
- @reflection.table_name
- end
-
- def construct_select(custom_select = nil)
- distinct = "DISTINCT " if @reflection.options[:uniq]
- selected = custom_select || @reflection.options[:select] || "#{distinct}#{@reflection.quoted_table_name}.*"
- end
-
- def construct_joins(custom_joins = nil)
- polymorphic_join = nil
- if @reflection.source_reflection.macro == :belongs_to
- reflection_primary_key = @reflection.klass.primary_key
- source_primary_key = @reflection.source_reflection.primary_key_name
- if @reflection.options[:source_type]
- polymorphic_join = "AND %s.%s = %s" % [
- @reflection.through_reflection.quoted_table_name, "#{@reflection.source_reflection.options[:foreign_type]}",
- @owner.class.quote_value(@reflection.options[:source_type])
- ]
- end
- else
- reflection_primary_key = @reflection.source_reflection.primary_key_name
- 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",
- @owner.class.quote_value(@reflection.through_reflection.klass.name)
- ]
- end
- end
-
- "INNER JOIN %s ON %s.%s = %s.%s %s #{@reflection.options[:joins]} #{custom_joins}" % [
- @reflection.through_reflection.quoted_table_name,
- @reflection.quoted_table_name, reflection_primary_key,
- @reflection.through_reflection.quoted_table_name, source_primary_key,
- polymorphic_join
- ]
- end
-
- # Construct attributes for associate pointing to owner.
- def construct_owner_attributes(reflection)
- if as = reflection.options[:as]
- { "#{as}_id" => @owner.id,
- "#{as}_type" => @owner.class.base_class.name.to_s }
- else
- { reflection.primary_key_name => @owner.id }
- end
- end
-
- # Construct attributes for :through pointing to owner and associate.
- def construct_join_attributes(associate)
- # TODO: revisit this to allow it for deletion, supposing dependent option is supported
- raise ActiveRecord::HasManyThroughCantAssociateThroughHasOneOrManyReflection.new(@owner, @reflection) if [:has_one, :has_many].include?(@reflection.source_reflection.macro)
-
- join_attributes = construct_owner_attributes(@reflection.through_reflection).merge(@reflection.source_reflection.primary_key_name => associate.id)
-
- if @reflection.options[:source_type]
- join_attributes.merge!(@reflection.source_reflection.options[:foreign_type] => associate.class.base_class.name.to_s)
- end
-
- if @reflection.through_reflection.options[:conditions].is_a?(Hash)
- join_attributes.merge!(@reflection.through_reflection.options[:conditions])
- end
-
- join_attributes
- end
-
- def conditions
- @conditions = build_conditions unless defined?(@conditions)
- @conditions
- end
-
- def build_conditions
- association_conditions = @reflection.options[:conditions]
- through_conditions = build_through_conditions
- source_conditions = @reflection.source_reflection.options[:conditions]
- uses_sti = !@reflection.through_reflection.klass.descends_from_active_record?
-
- if association_conditions || through_conditions || source_conditions || uses_sti
- all = []
-
- [association_conditions, source_conditions].each do |conditions|
- all << interpolate_sql(sanitize_sql(conditions)) if conditions
- end
-
- all << through_conditions if through_conditions
- all << build_sti_condition if uses_sti
-
- all.map { |sql| "(#{sql})" } * ' AND '
- end
- end
-
- def build_through_conditions
- conditions = @reflection.through_reflection.options[:conditions]
- if conditions.is_a?(Hash)
- interpolate_sql(@reflection.through_reflection.klass.send(:sanitize_sql, conditions)).gsub(
- @reflection.quoted_table_name,
- @reflection.through_reflection.quoted_table_name)
- elsif conditions
- interpolate_sql(sanitize_sql(conditions))
- end
- end
-
- def build_sti_condition
- @reflection.through_reflection.klass.send(:type_condition).to_sql
- end
-
- alias_method :sql_conditions, :conditions
- end
- end
-end
diff --git a/activerecord/lib/active_record/attribute_methods.rb b/activerecord/lib/active_record/attribute_methods.rb
index 56e18eced0..5833c65893 100644
--- a/activerecord/lib/active_record/attribute_methods.rb
+++ b/activerecord/lib/active_record/attribute_methods.rb
@@ -10,7 +10,18 @@ module ActiveRecord
# Generates all the attribute related methods for columns in the database
# accessors, mutators and query methods.
def define_attribute_methods
- super(columns_hash.keys)
+ return if attribute_methods_generated?
+ super(column_names)
+ @attribute_methods_generated = true
+ end
+
+ def attribute_methods_generated?
+ @attribute_methods_generated ||= false
+ end
+
+ def undefine_attribute_methods(*args)
+ super
+ @attribute_methods_generated = false
end
# Checks whether the method is defined in the model or any of its subclasses
@@ -18,7 +29,11 @@ module ActiveRecord
# method is defined by Active Record though.
def instance_method_already_implemented?(method_name)
method_name = method_name.to_s
- @_defined_class_methods ||= ancestors.first(ancestors.index(ActiveRecord::Base)).sum([]) { |m| m.public_instance_methods(false) | m.private_instance_methods(false) | m.protected_instance_methods(false) }.map {|m| m.to_s }.to_set
+ index = ancestors.index(ActiveRecord::Base) || ancestors.length
+ @_defined_class_methods ||= ancestors.first(index).map { |m|
+ m.instance_methods(false) | m.private_instance_methods(false)
+ }.flatten.map {|m| m.to_s }.to_set
+
@@_defined_activerecord_methods ||= defined_activerecord_methods
raise DangerousAttributeError, "#{method_name} is defined by ActiveRecord" if @@_defined_activerecord_methods.include?(method_name)
@_defined_class_methods.include?(method_name)
@@ -27,9 +42,8 @@ module ActiveRecord
def defined_activerecord_methods
active_record = ActiveRecord::Base
super_klass = ActiveRecord::Base.superclass
- methods = active_record.public_instance_methods - super_klass.public_instance_methods
- methods += active_record.private_instance_methods - super_klass.private_instance_methods
- methods += active_record.protected_instance_methods - super_klass.protected_instance_methods
+ methods = (active_record.instance_methods - super_klass.instance_methods) +
+ (active_record.private_instance_methods - super_klass.private_instance_methods)
methods.map {|m| m.to_s }.to_set
end
end
@@ -48,13 +62,13 @@ module ActiveRecord
end
def respond_to?(*args)
- self.class.define_attribute_methods
+ self.class.define_attribute_methods unless self.class.attribute_methods_generated?
super
end
protected
def attribute_method?(attr_name)
- attr_name == 'id' || attributes.include?(attr_name)
+ attr_name == 'id' || (defined?(@attributes) && @attributes.include?(attr_name))
end
end
end
diff --git a/activerecord/lib/active_record/attribute_methods/before_type_cast.rb b/activerecord/lib/active_record/attribute_methods/before_type_cast.rb
index a4e144f233..bde11d0494 100644
--- a/activerecord/lib/active_record/attribute_methods/before_type_cast.rb
+++ b/activerecord/lib/active_record/attribute_methods/before_type_cast.rb
@@ -13,21 +13,19 @@ module ActiveRecord
# Returns a hash of attributes before typecasting and deserialization.
def attributes_before_type_cast
- self.attribute_names.inject({}) do |attrs, name|
- attrs[name] = read_attribute_before_type_cast(name)
- attrs
- end
+ @attributes
end
private
- # Handle *_before_type_cast for method_missing.
- def attribute_before_type_cast(attribute_name)
- if attribute_name == 'id'
- read_attribute_before_type_cast(self.class.primary_key)
- else
- read_attribute_before_type_cast(attribute_name)
- end
+
+ # Handle *_before_type_cast for method_missing.
+ def attribute_before_type_cast(attribute_name)
+ if attribute_name == 'id'
+ read_attribute_before_type_cast(self.class.primary_key)
+ else
+ read_attribute_before_type_cast(attribute_name)
end
+ end
end
end
end
diff --git a/activerecord/lib/active_record/attribute_methods/dirty.rb b/activerecord/lib/active_record/attribute_methods/dirty.rb
index cf4594ad7f..3eff3d54e3 100644
--- a/activerecord/lib/active_record/attribute_methods/dirty.rb
+++ b/activerecord/lib/active_record/attribute_methods/dirty.rb
@@ -1,3 +1,4 @@
+require 'active_support/core_ext/class/attribute'
require 'active_support/core_ext/object/blank'
module ActiveRecord
@@ -12,7 +13,7 @@ module ActiveRecord
raise "You cannot include Dirty after Timestamp"
end
- superclass_delegating_accessor :partial_updates
+ class_attribute :partial_updates
self.partial_updates = true
end
@@ -21,6 +22,8 @@ module ActiveRecord
if status = super
@previously_changed = changes
@changed_attributes.clear
+ elsif IdentityMap.enabled?
+ IdentityMap.remove(self)
end
status
end
@@ -31,6 +34,9 @@ module ActiveRecord
@previously_changed = changes
@changed_attributes.clear
end
+ rescue
+ IdentityMap.remove(self) if IdentityMap.enabled?
+ raise
end
# <tt>reload</tt> the record and clears changed attributes.
@@ -88,7 +94,7 @@ module ActiveRecord
end
def clone_with_time_zone_conversion_attribute?(attr, old)
- old.class.name == "Time" && time_zone_aware_attributes && !skip_time_zone_conversion_for_attributes.include?(attr.to_sym)
+ old.class.name == "Time" && time_zone_aware_attributes && !self.skip_time_zone_conversion_for_attributes.include?(attr.to_sym)
end
end
end
diff --git a/activerecord/lib/active_record/attribute_methods/primary_key.rb b/activerecord/lib/active_record/attribute_methods/primary_key.rb
index 82d94b848a..fcdd31ddea 100644
--- a/activerecord/lib/active_record/attribute_methods/primary_key.rb
+++ b/activerecord/lib/active_record/attribute_methods/primary_key.rb
@@ -3,36 +3,48 @@ module ActiveRecord
module PrimaryKey
extend ActiveSupport::Concern
- # Returns this record's primary key value wrapped in an Array
- # or nil if the record is a new_record?
+ # Returns this record's primary key value wrapped in an Array or nil if
+ # the record is not persisted? or has just been destroyed.
def to_key
- new_record? ? nil : [ id ]
+ key = send(self.class.primary_key)
+ [key] if key
end
module ClassMethods
# Defines the primary key field -- can be overridden in subclasses. Overwriting will negate any effect of the
# primary_key_prefix_type setting, though.
def primary_key
- reset_primary_key
+ @primary_key ||= reset_primary_key
end
def reset_primary_key #:nodoc:
- key = get_primary_key(base_class.name)
+ key = self == base_class ? get_primary_key(base_class.name) :
+ base_class.primary_key
+
set_primary_key(key)
key
end
def get_primary_key(base_name) #:nodoc:
- key = 'id'
+ return 'id' unless base_name && !base_name.blank?
+
case primary_key_prefix_type
- when :table_name
- key = base_name.to_s.foreign_key(false)
- when :table_name_with_underscore
- key = base_name.to_s.foreign_key
+ when :table_name
+ base_name.foreign_key(false)
+ when :table_name_with_underscore
+ base_name.foreign_key
+ else
+ if ActiveRecord::Base != self && connection.table_exists?(table_name)
+ connection.primary_key(table_name)
+ else
+ 'id'
+ end
end
- key
end
+ attr_accessor :original_primary_key
+ attr_writer :primary_key
+
# Sets the name of the primary key column to use to the given value,
# or (if the value is nil or false) to the value returned by the given
# block.
@@ -41,9 +53,12 @@ module ActiveRecord
# set_primary_key "sysid"
# end
def set_primary_key(value = nil, &block)
- define_attr_method :primary_key, value, &block
+ @primary_key ||= ''
+ self.original_primary_key = @primary_key
+ value &&= value.to_s
+ connection_pool.primary_keys[table_name] = value
+ self.primary_key = block_given? ? instance_eval(&block) : value
end
- alias :primary_key= :set_primary_key
end
end
end
diff --git a/activerecord/lib/active_record/attribute_methods/read.rb b/activerecord/lib/active_record/attribute_methods/read.rb
index 3da3d9d8cc..69d5cd83f1 100644
--- a/activerecord/lib/active_record/attribute_methods/read.rb
+++ b/activerecord/lib/active_record/attribute_methods/read.rb
@@ -20,14 +20,13 @@ module ActiveRecord
# be cached. Usually caching only pays off for attributes with expensive conversion
# methods, like time related columns (e.g. +created_at+, +updated_at+).
def cache_attributes(*attribute_names)
- attribute_names.each {|attr| cached_attributes << attr.to_s}
+ cached_attributes.merge attribute_names.map { |attr| attr.to_s }
end
# Returns the attributes which are cached. By default time related columns
# with datatype <tt>:datetime, :timestamp, :time, :date</tt> are cached.
def cached_attributes
- @cached_attributes ||=
- columns.select{|c| attribute_types_cached_by_default.include?(c.type)}.map{|col| col.name}.to_set
+ @cached_attributes ||= columns.select { |c| cacheable_column?(c) }.map { |col| col.name }.to_set
end
# Returns +true+ if the provided attribute is being cached.
@@ -37,10 +36,10 @@ module ActiveRecord
protected
def define_method_attribute(attr_name)
- if self.serialized_attributes[attr_name]
+ if serialized_attributes.include?(attr_name)
define_read_method_for_serialized_attribute(attr_name)
else
- define_read_method(attr_name.to_sym, attr_name, columns_hash[attr_name])
+ define_read_method(attr_name, attr_name, columns_hash[attr_name])
end
if attr_name == primary_key && attr_name != "id"
@@ -49,33 +48,52 @@ module ActiveRecord
end
private
+ def cacheable_column?(column)
+ serialized_attributes.include?(column.name) || attribute_types_cached_by_default.include?(column.type)
+ end
+
# Define read method for serialized attribute.
def define_read_method_for_serialized_attribute(attr_name)
- generated_attribute_methods.module_eval("def #{attr_name}; unserialize_attribute('#{attr_name}'); end", __FILE__, __LINE__)
+ access_code = "@attributes_cache['#{attr_name}'] ||= @attributes['#{attr_name}']"
+ generated_attribute_methods.module_eval("def _#{attr_name}; #{access_code}; end; alias #{attr_name} _#{attr_name}", __FILE__, __LINE__)
end
# Define an attribute reader method. Cope with nil column.
def define_read_method(symbol, attr_name, column)
- cast_code = column.type_cast_code('v') if column
- access_code = cast_code ? "(v=@attributes['#{attr_name}']) && #{cast_code}" : "@attributes['#{attr_name}']"
+ cast_code = column.type_cast_code('v')
+ access_code = "(v=@attributes['#{attr_name}']) && #{cast_code}"
unless attr_name.to_s == self.primary_key.to_s
- access_code = access_code.insert(0, "missing_attribute('#{attr_name}', caller) unless @attributes.has_key?('#{attr_name}'); ")
+ access_code.insert(0, "missing_attribute('#{attr_name}', caller) unless @attributes.has_key?('#{attr_name}'); ")
end
if cache_attribute?(attr_name)
access_code = "@attributes_cache['#{attr_name}'] ||= (#{access_code})"
end
- generated_attribute_methods.module_eval("def #{symbol}; #{access_code}; end", __FILE__, __LINE__)
+ if symbol =~ /^[a-zA-Z_]\w*[!?=]?$/
+ generated_attribute_methods.module_eval("def _#{symbol}; #{access_code}; end; alias #{symbol} _#{symbol}", __FILE__, __LINE__)
+ else
+ generated_attribute_methods.module_eval do
+ define_method("_#{symbol}") { eval(access_code) }
+ alias_method(symbol, "_#{symbol}")
+ end
+ end
end
end
# Returns the value of the attribute identified by <tt>attr_name</tt> after it has been typecast (for example,
# "2004-12-12" in a data column is cast to a date object, like Date.new(2004, 12, 12)).
def read_attribute(attr_name)
+ send "_#{attr_name}"
+ rescue NoMethodError
+ _read_attribute attr_name
+ end
+
+ def _read_attribute(attr_name)
attr_name = attr_name.to_s
attr_name = self.class.primary_key if attr_name == 'id'
- if !(value = @attributes[attr_name]).nil?
+ value = @attributes[attr_name]
+ unless value.nil?
if column = column_for_attribute(attr_name)
if unserializable_attribute?(attr_name, column)
unserialize_attribute(attr_name)
@@ -85,26 +103,20 @@ module ActiveRecord
else
value
end
- else
- nil
end
end
# Returns true if the attribute is of a text column and marked for serialization.
def unserializable_attribute?(attr_name, column)
- column.text? && self.class.serialized_attributes[attr_name]
+ column.text? && self.class.serialized_attributes.include?(attr_name)
end
# Returns the unserialized object of the attribute.
def unserialize_attribute(attr_name)
- unserialized_object = object_from_yaml(@attributes[attr_name])
+ coder = self.class.serialized_attributes[attr_name]
+ unserialized_object = coder.load(@attributes[attr_name])
- if unserialized_object.is_a?(self.class.serialized_attributes[attr_name]) || unserialized_object.nil?
- @attributes.frozen? ? unserialized_object : @attributes[attr_name] = unserialized_object
- else
- raise SerializationTypeMismatch,
- "#{attr_name} was supposed to be a #{self.class.serialized_attributes[attr_name]}, but was a #{unserialized_object.class.to_s}"
- end
+ @attributes.frozen? ? unserialized_object : @attributes[attr_name] = unserialized_object
end
private
diff --git a/activerecord/lib/active_record/attribute_methods/time_zone_conversion.rb b/activerecord/lib/active_record/attribute_methods/time_zone_conversion.rb
index 8f0aacba42..6aac96df6f 100644
--- a/activerecord/lib/active_record/attribute_methods/time_zone_conversion.rb
+++ b/activerecord/lib/active_record/attribute_methods/time_zone_conversion.rb
@@ -1,3 +1,5 @@
+require 'active_support/core_ext/class/attribute'
+
module ActiveRecord
module AttributeMethods
module TimeZoneConversion
@@ -7,24 +9,25 @@ module ActiveRecord
cattr_accessor :time_zone_aware_attributes, :instance_writer => false
self.time_zone_aware_attributes = false
- class_inheritable_accessor :skip_time_zone_conversion_for_attributes, :instance_writer => false
+ class_attribute :skip_time_zone_conversion_for_attributes, :instance_writer => false
self.skip_time_zone_conversion_for_attributes = []
end
module ClassMethods
protected
# Defined for all +datetime+ and +timestamp+ attributes when +time_zone_aware_attributes+ are enabled.
- # This enhanced read method automatically converts the UTC time stored in the database to the time
+ # This enhanced read method automatically converts the UTC time stored in the database to the time
# zone stored in Time.zone.
def define_method_attribute(attr_name)
if create_time_zone_conversion_attribute?(attr_name, columns_hash[attr_name])
method_body, line = <<-EOV, __LINE__ + 1
- def #{attr_name}(reload = false)
+ def _#{attr_name}
cached = @attributes_cache['#{attr_name}']
- return cached if cached && !reload
- time = read_attribute('#{attr_name}')
+ return cached if cached
+ time = _read_attribute('#{attr_name}')
@attributes_cache['#{attr_name}'] = time.acts_like?(:time) ? time.in_time_zone : time
end
+ alias #{attr_name} _#{attr_name}
EOV
generated_attribute_methods.module_eval(method_body, __FILE__, line)
else
@@ -37,12 +40,14 @@ module ActiveRecord
def define_method_attribute=(attr_name)
if create_time_zone_conversion_attribute?(attr_name, columns_hash[attr_name])
method_body, line = <<-EOV, __LINE__ + 1
- def #{attr_name}=(time)
+ def #{attr_name}=(original_time)
+ time = original_time
unless time.acts_like?(:time)
time = time.is_a?(String) ? Time.zone.parse(time) : time.to_time rescue time
end
time = time.in_time_zone rescue nil if time
- write_attribute(:#{attr_name}, time)
+ write_attribute(:#{attr_name}, original_time)
+ @attributes_cache["#{attr_name}"] = time
end
EOV
generated_attribute_methods.module_eval(method_body, __FILE__, line)
@@ -53,7 +58,7 @@ module ActiveRecord
private
def create_time_zone_conversion_attribute?(name, column)
- time_zone_aware_attributes && !skip_time_zone_conversion_for_attributes.include?(name.to_sym) && [:datetime, :timestamp].include?(column.type)
+ time_zone_aware_attributes && !self.skip_time_zone_conversion_for_attributes.include?(name.to_sym) && [:datetime, :timestamp].include?(column.type)
end
end
end
diff --git a/activerecord/lib/active_record/attribute_methods/write.rb b/activerecord/lib/active_record/attribute_methods/write.rb
index 7a2de3bf80..3c4dab304e 100644
--- a/activerecord/lib/active_record/attribute_methods/write.rb
+++ b/activerecord/lib/active_record/attribute_methods/write.rb
@@ -10,11 +10,17 @@ module ActiveRecord
module ClassMethods
protected
def define_method_attribute=(attr_name)
- generated_attribute_methods.module_eval("def #{attr_name}=(new_value); write_attribute('#{attr_name}', new_value); end", __FILE__, __LINE__)
+ if attr_name =~ /^[a-zA-Z_]\w*[!?=]?$/
+ generated_attribute_methods.module_eval("def #{attr_name}=(new_value); write_attribute('#{attr_name}', new_value); end", __FILE__, __LINE__)
+ else
+ generated_attribute_methods.send(:define_method, "#{attr_name}=") do |new_value|
+ write_attribute(attr_name, new_value)
+ end
+ end
end
end
- # Updates the attribute identified by <tt>attr_name</tt> with the specified +value+. Empty strings
+ # Updates the attribute identified by <tt>attr_name</tt> with the specified +value+. Empty strings
# for fixnum and float columns are turned into +nil+.
def write_attribute(attr_name, value)
attr_name = attr_name.to_s
diff --git a/activerecord/lib/active_record/autosave_association.rb b/activerecord/lib/active_record/autosave_association.rb
index 2c7afe3c9f..48dbe0838a 100644
--- a/activerecord/lib/active_record/autosave_association.rb
+++ b/activerecord/lib/active_record/autosave_association.rb
@@ -2,14 +2,14 @@ require 'active_support/core_ext/array/wrap'
module ActiveRecord
# = Active Record Autosave Association
- #
- # AutosaveAssociation is a module that takes care of automatically saving
- # associacted records when parent is saved. In addition to saving, it
+ #
+ # +AutosaveAssociation+ is a module that takes care of automatically saving
+ # associated records when their parent is saved. In addition to saving, it
# also destroys any associated records that were marked for destruction.
- # (See mark_for_destruction and marked_for_destruction?)
+ # (See +mark_for_destruction+ and <tt>marked_for_destruction?</tt>).
#
# Saving of the parent, its associations, and the destruction of marked
- # associations, all happen inside 1 transaction. This should never leave the
+ # associations, all happen inside a transaction. This should never leave the
# database in an inconsistent state.
#
# If validations for any of the associations fail, their error messages will
@@ -18,6 +18,9 @@ module ActiveRecord
# Note that it also means that associations marked for destruction won't
# be destroyed directly. They will however still be marked for destruction.
#
+ # Note that <tt>:autosave => false</tt> is not same as not declaring <tt>:autosave</tt>.
+ # When the <tt>:autosave</tt> option is not present new associations are saved.
+ #
# === One-to-one Example
#
# class Post
@@ -28,7 +31,7 @@ module ActiveRecord
# automatically _and_ atomically:
#
# post = Post.find(1)
- # post.title # => "The current global position of migrating ducks"
+ # post.title # => "The current global position of migrating ducks"
# post.author.name # => "alloy"
#
# post.title = "On the migration of ducks"
@@ -36,7 +39,7 @@ module ActiveRecord
#
# post.save
# post.reload
- # post.title # => "On the migration of ducks"
+ # 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
@@ -46,6 +49,7 @@ module ActiveRecord
# 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
#
@@ -53,40 +57,49 @@ module ActiveRecord
# 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:
+ # When <tt>:autosave</tt> is not declared new children are saved when their parent is saved:
#
# class Post
- # has_many :comments, :autosave => true
+ # has_many :comments # :autosave option is no declared
# end
#
- # Saving changes to the parent and its associated model can now be performed
- # automatically _and_ atomically:
+ # post = Post.new(:title => 'ruby rocks')
+ # post.comments.build(:body => 'hello world')
+ # post.save # => saves both post and comment
#
- # 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 = Post.create(:title => 'ruby rocks')
+ # post.comments.build(:body => 'hello world')
+ # post.save # => saves both post and comment
#
- # post.title = "On the migration of ducks"
- # post.comments.last.body = "Actually, your article should be named differently. [UPDATED]: You are right, thanks."
+ # post = Post.create(:title => 'ruby rocks')
+ # post.comments.create(:body => 'hello world')
+ # post.save # => saves both post and comment
#
- # 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."
+ # When <tt>:autosave</tt> is true all children is saved, no matter whether they are new records:
#
- # Destroying one of the associated models members, as part of the parent's
- # save action, is as simple as marking it for destruction:
+ # class Post
+ # has_many :comments, :autosave => true
+ # end
+ #
+ # post = Post.create(:title => 'ruby rocks')
+ # post.comments.create(:body => 'hello world')
+ # post.comments[0].body = 'hi everyone'
+ # post.save # => saves both post and comment, with 'hi everyone' as body
+ #
+ # Destroying one of the associated models 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
#
@@ -94,62 +107,53 @@ module ActiveRecord
# 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(:validate => false) # => true
+ # Children records are validated unless <tt>:validate</tt> is +false+.
module AutosaveAssociation
extend ActiveSupport::Concern
- ASSOCIATION_TYPES = %w{ has_one belongs_to has_many has_and_belongs_to_many }
+ ASSOCIATION_TYPES = %w{ HasOne HasMany BelongsTo HasAndBelongsToMany }
+
+ module AssociationBuilderExtension #:nodoc:
+ def self.included(base)
+ base.valid_options << :autosave
+ end
+
+ def build
+ reflection = super
+ model.send(:add_autosave_association_callbacks, reflection)
+ reflection
+ end
+ end
included do
ASSOCIATION_TYPES.each do |type|
- send("valid_keys_for_#{type}_association") << :autosave
+ Associations::Builder.const_get(type).send(:include, AssociationBuilderExtension)
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 <<-CODE, __FILE__, __LINE__ + 1
- def #{type}(name, options = {})
- super
- add_autosave_association_callbacks(reflect_on_association(name))
+ def define_non_cyclic_method(name, reflection, &block)
+ define_method(name) do |*args|
+ result = true; @_already_called ||= {}
+ # Loop prevention for validation of associations
+ unless @_already_called[[name, reflection.name]]
+ begin
+ @_already_called[[name, reflection.name]]=true
+ result = instance_eval(&block)
+ ensure
+ @_already_called[[name, reflection.name]]=false
+ end
end
- CODE
+
+ result
+ end
end
# Adds validation and save callbacks for the association as specified by
@@ -172,16 +176,25 @@ module ActiveRecord
if collection
before_save :before_save_collection_association
- define_method(save_method) { save_collection_association(reflection) }
+ define_non_cyclic_method(save_method, reflection) { 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
else
if reflection.macro == :has_one
define_method(save_method) { save_has_one_association(reflection) }
- after_save save_method
+ # Configures two callbacks instead of a single after_save so that
+ # the model may rely on their execution order relative to its
+ # own callbacks.
+ #
+ # For example, given that after_creates run before after_saves, if
+ # we configured instead an after_save there would be no way to fire
+ # a custom after_create callback after the child association gets
+ # created.
+ after_create save_method
+ after_update save_method
else
- define_method(save_method) { save_belongs_to_association(reflection) }
+ define_non_cyclic_method(save_method, reflection) { save_belongs_to_association(reflection) }
before_save save_method
end
end
@@ -189,7 +202,7 @@ module ActiveRecord
if reflection.validate? && !method_defined?(validation_method)
method = (collection ? :validate_collection_association : :validate_single_association)
- define_method(validation_method) { send(method, reflection) }
+ define_non_cyclic_method(validation_method, reflection) { send(method, reflection) }
validate validation_method
end
end
@@ -202,7 +215,7 @@ module ActiveRecord
end
# Marks this record to be destroyed as part of the parents save transaction.
- # This does _not_ actually destroy the record instantly, rather child record will be destroyed
+ # This does _not_ actually destroy the record instantly, rather child record 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.
@@ -222,7 +235,7 @@ module ActiveRecord
def changed_for_autosave?
new_record? || changed? || marked_for_destruction? || nested_records_changed_for_autosave?
end
-
+
private
# Returns the record for an association collection that should be validated
@@ -230,7 +243,7 @@ module ActiveRecord
# unless the parent is/was a new record itself.
def associated_records_to_validate_or_save(association, new_record, autosave)
if new_record
- association
+ association && association.target
elsif autosave
association.target.find_all { |record| record.changed_for_autosave? }
else
@@ -243,16 +256,16 @@ module ActiveRecord
def nested_records_changed_for_autosave?
self.class.reflect_on_all_autosave_associations.any? do |reflection|
association = association_instance_get(reflection.name)
- association && Array.wrap(association.target).any?(&:changed_for_autosave?)
+ association && Array.wrap(association.target).any? { |a| a.changed_for_autosave? }
end
end
-
+
# Validate the association if <tt>:validate</tt> or <tt>:autosave</tt> is
# turned on for the association.
def validate_single_association(reflection)
- if (association = association_instance_get(reflection.name)) && !association.target.nil?
- association_valid?(reflection, association)
- end
+ association = association_instance_get(reflection.name)
+ record = association && association.target
+ association_valid?(reflection, record) if record
end
# Validate the associated records if <tt>:validate</tt> or
@@ -269,12 +282,12 @@ module ActiveRecord
# 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? or destroyed.
- def association_valid?(reflection, association)
- return true if association.destroyed? || association.marked_for_destruction?
+ def association_valid?(reflection, record)
+ return true if record.destroyed? || record.marked_for_destruction?
- unless valid = association.valid?
+ unless valid = record.valid?
if reflection.options[:autosave]
- association.errors.each do |attribute, message|
+ record.errors.each do |attribute, message|
attribute = "#{reflection.name}.#{attribute}"
errors[attribute] << message
errors[attribute].uniq!
@@ -306,27 +319,35 @@ module ActiveRecord
autosave = reflection.options[:autosave]
if records = associated_records_to_validate_or_save(association, @new_record_before_save, autosave)
+ begin
records.each do |record|
next if record.destroyed?
+ saved = true
+
if autosave && record.marked_for_destruction?
- association.destroy(record)
+ association.proxy.destroy(record)
elsif autosave != false && (@new_record_before_save || record.new_record?)
if autosave
- saved = association.send(:insert_record, record, false, false)
+ saved = association.insert_record(record, false)
else
- association.send(:insert_record, record)
+ association.insert_record(record)
end
elsif autosave
saved = record.save(:validate => false)
end
- raise ActiveRecord::Rollback if saved == false
+ raise ActiveRecord::Rollback unless saved
end
+ rescue
+ records.each {|x| IdentityMap.remove(x) } if IdentityMap.enabled?
+ raise
+ end
+
end
- # reconstruct the SQL queries now that we know the owner's id
- association.send(:construct_sql) if association.respond_to?(:construct_sql)
+ # reconstruct the scope now that we know the owner's id
+ association.send(:construct_scope) if association.respond_to?(:construct_scope)
end
end
@@ -339,16 +360,18 @@ module ActiveRecord
# 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? && !association.destroyed?
+ association = association_instance_get(reflection.name)
+ record = association && association.load_target
+ if record && !record.destroyed?
autosave = reflection.options[:autosave]
- if autosave && association.marked_for_destruction?
- association.destroy
+ if autosave && record.marked_for_destruction?
+ record.destroy
else
key = reflection.options[:primary_key] ? send(reflection.options[:primary_key]) : id
- if autosave != false && (new_record? || association.new_record? || association[reflection.primary_key_name] != key || autosave)
- association[reflection.primary_key_name] = key
- saved = association.save(:validate => !autosave)
+ if autosave != false && (new_record? || record.new_record? || record[reflection.foreign_key] != key || autosave)
+ record[reflection.foreign_key] = key
+ saved = record.save(:validate => !autosave)
raise ActiveRecord::Rollback if !saved && autosave
saved
end
@@ -360,17 +383,20 @@ module ActiveRecord
#
# In addition, it will destroy the association if it was marked for destruction.
def save_belongs_to_association(reflection)
- if (association = association_instance_get(reflection.name)) && !association.destroyed?
+ association = association_instance_get(reflection.name)
+ record = association && association.load_target
+ if record && !record.destroyed?
autosave = reflection.options[:autosave]
- if autosave && association.marked_for_destruction?
- association.destroy
+ if autosave && record.marked_for_destruction?
+ record.destroy
elsif autosave != false
- saved = association.save(:validate => !autosave) if association.new_record? || autosave
+ saved = record.save(:validate => !autosave) if record.new_record? || (autosave && record.changed_for_autosave?)
if association.updated?
- association_id = association.send(reflection.options[:primary_key] || :id)
- self[reflection.primary_key_name] = association_id
+ association_id = record.send(reflection.options[:primary_key] || :id)
+ self[reflection.foreign_key] = association_id
+ association.loaded!
end
saved if autosave
diff --git a/activerecord/lib/active_record/base.rb b/activerecord/lib/active_record/base.rb
index 8da4fbcba7..b778b0c0f0 100644
--- a/activerecord/lib/active_record/base.rb
+++ b/activerecord/lib/active_record/base.rb
@@ -1,3 +1,8 @@
+begin
+ require 'psych'
+rescue LoadError
+end
+
require 'yaml'
require 'set'
require 'active_support/benchmarkable'
@@ -7,7 +12,7 @@ require 'active_support/time'
require 'active_support/core_ext/class/attribute'
require 'active_support/core_ext/class/attribute_accessors'
require 'active_support/core_ext/class/delegating_attributes'
-require 'active_support/core_ext/class/inheritable_attributes'
+require 'active_support/core_ext/class/attribute'
require 'active_support/core_ext/array/extract_options'
require 'active_support/core_ext/hash/deep_merge'
require 'active_support/core_ext/hash/indifferent_access'
@@ -15,7 +20,6 @@ require 'active_support/core_ext/hash/slice'
require 'active_support/core_ext/string/behavior'
require 'active_support/core_ext/kernel/singleton_class'
require 'active_support/core_ext/module/delegation'
-require 'active_support/core_ext/module/deprecation'
require 'active_support/core_ext/module/introspection'
require 'active_support/core_ext/object/duplicable'
require 'active_support/core_ext/object/blank'
@@ -26,18 +30,18 @@ require 'active_record/log_subscriber'
module ActiveRecord #:nodoc:
# = Active Record
#
- # Active Record objects don't specify their attributes directly, but rather infer them from
- # the table definition with which they're linked. Adding, removing, and changing attributes
- # and their type is done directly in the database. Any change is instantly reflected in the
+ # Active Record objects don't specify their attributes directly, but rather infer them from
+ # the table definition with which they're linked. Adding, removing, and changing attributes
+ # and their type is done directly in the database. Any change is instantly reflected in the
# Active Record objects. The mapping that binds a given Active Record class to a certain
# database table will happen automatically in most common cases, but can be overwritten for the uncommon ones.
#
- # See the mapping rules in table_name and the full example in link:files/README.html for more insight.
+ # See the mapping rules in table_name and the full example in link:files/activerecord/README_rdoc.html for more insight.
#
# == Creation
#
- # Active Records accept constructor parameters either in a hash or as a block. The hash
- # method is especially useful when you're receiving the data from somewhere else, like an
+ # Active Records accept constructor parameters either in a hash or as a block. The hash
+ # method is especially useful when you're receiving the data from somewhere else, like an
# HTTP request. It works like this:
#
# user = User.new(:name => "David", :occupation => "Code Artist")
@@ -77,16 +81,16 @@ module ActiveRecord #:nodoc:
# end
# end
#
- # The <tt>authenticate_unsafely</tt> method inserts the parameters directly into the query
- # and is thus susceptible to SQL-injection attacks if the <tt>user_name</tt> and +password+
+ # The <tt>authenticate_unsafely</tt> method inserts the parameters directly into the query
+ # and is thus susceptible to SQL-injection attacks if the <tt>user_name</tt> and +password+
# parameters come directly from an HTTP request. The <tt>authenticate_safely</tt> and
- # <tt>authenticate_safely_simply</tt> both will sanitize the <tt>user_name</tt> and +password+
- # before inserting them in the query, which will ensure that an attacker can't escape the
+ # <tt>authenticate_safely_simply</tt> both will sanitize the <tt>user_name</tt> and +password+
+ # before inserting them in the query, which will ensure that an attacker can't escape the
# query and fake the login (or worse).
#
- # When using multiple parameters in the conditions, it can easily become hard to read exactly
- # what the fourth or fifth question mark is supposed to represent. In those cases, you can
- # resort to named bind variables instead. That's done by replacing the question marks with
+ # When using multiple parameters in the conditions, it can easily become hard to read exactly
+ # what the fourth or fifth question mark is supposed to represent. In those cases, you can
+ # resort to named bind variables instead. That's done by replacing the question marks with
# symbols and supplying a hash with values for the matching symbol keys:
#
# Company.where(
@@ -108,7 +112,7 @@ module ActiveRecord #:nodoc:
#
# Student.where(:grade => [9,11,12])
#
- # When joining tables, nested hashes or keys written in the form 'table_name.column_name'
+ # When joining tables, nested hashes or keys written in the form 'table_name.column_name'
# can be used to qualify the table name of a particular condition. For instance:
#
# Student.joins(:schools).where(:schools => { :type => 'public' })
@@ -116,10 +120,10 @@ module ActiveRecord #:nodoc:
#
# == Overwriting default accessors
#
- # All column values are automatically available through basic accessors on the Active Record
- # object, but sometimes you want to specialize this behavior. This can be done by overwriting
- # the default accessors (using the same name as the attribute) and calling
- # <tt>read_attribute(attr_name)</tt> and <tt>write_attribute(attr_name, value)</tt> to actually
+ # All column values are automatically available through basic accessors on the Active Record
+ # object, but sometimes you want to specialize this behavior. This can be done by overwriting
+ # the default accessors (using the same name as the attribute) and calling
+ # <tt>read_attribute(attr_name)</tt> and <tt>write_attribute(attr_name, value)</tt> to actually
# change things.
#
# class Song < ActiveRecord::Base
@@ -134,7 +138,7 @@ module ActiveRecord #:nodoc:
# end
# end
#
- # You can alternatively use <tt>self[:attribute]=(value)</tt> and <tt>self[:attribute]</tt>
+ # You can alternatively use <tt>self[:attribute]=(value)</tt> and <tt>self[:attribute]</tt>
# instead of <tt>write_attribute(:attribute, value)</tt> and <tt>read_attribute(:attribute)</tt>.
#
# == Attribute query methods
@@ -153,43 +157,40 @@ module ActiveRecord #:nodoc:
#
# == Accessing attributes before they have been typecasted
#
- # Sometimes you want to be able to read the raw attribute data without having the column-determined
- # typecast run its course first. That can be done by using the <tt><attribute>_before_type_cast</tt>
- # accessors that all attributes have. For example, if your Account model has a <tt>balance</tt> attribute,
+ # Sometimes you want to be able to read the raw attribute data without having the column-determined
+ # typecast run its course first. That can be done by using the <tt><attribute>_before_type_cast</tt>
+ # accessors that all attributes have. For example, if your Account model has a <tt>balance</tt> attribute,
# you can call <tt>account.balance_before_type_cast</tt> or <tt>account.id_before_type_cast</tt>.
#
- # This is especially useful in validation situations where the user might supply a string for an
- # integer field and you want to display the original string back in an error message. Accessing the
+ # This is especially useful in validation situations where the user might supply a string for an
+ # integer field and you want to display the original string back in an error message. Accessing the
# attribute normally would typecast the string to 0, which isn't what you want.
#
# == Dynamic attribute-based finders
#
- # Dynamic attribute-based finders are a cleaner way of getting (and/or creating) objects
- # by simple queries without turning to SQL. They work by appending the name of an attribute
- # to <tt>find_by_</tt>, <tt>find_last_by_</tt>, or <tt>find_all_by_</tt> and thus produces finders
- # like <tt>Person.find_by_user_name</tt>, <tt>Person.find_all_by_last_name</tt>, and
- # <tt>Payment.find_by_transaction_id</tt>. Instead of writing
+ # Dynamic attribute-based finders are a cleaner way of getting (and/or creating) objects
+ # by simple queries without turning to SQL. They work by appending the name of an attribute
+ # to <tt>find_by_</tt>, <tt>find_last_by_</tt>, or <tt>find_all_by_</tt> and thus produces finders
+ # like <tt>Person.find_by_user_name</tt>, <tt>Person.find_all_by_last_name</tt>, and
+ # <tt>Payment.find_by_transaction_id</tt>. Instead of writing
# <tt>Person.where(:user_name => user_name).first</tt>, you just do <tt>Person.find_by_user_name(user_name)</tt>.
- # And instead of writing <tt>Person.where(:last_name => last_name).all</tt>, you just do
+ # And instead of writing <tt>Person.where(:last_name => last_name).all</tt>, you just do
# <tt>Person.find_all_by_last_name(last_name)</tt>.
#
# It's also possible to use multiple attributes in the same find by separating them with "_and_".
- #
+ #
# Person.where(:user_name => user_name, :password => password).first
- # Person.find_by_user_name_and_password #with dynamic finder
- #
- # Person.where(:user_name => user_name, :password => password, :gender => 'male').first
- # Payment.find_by_user_name_and_password_and_gender
+ # Person.find_by_user_name_and_password(user_name, password) # with dynamic finder
#
# It's even possible to call these dynamic finder methods on relations and named scopes.
#
# Payment.order("created_on").find_all_by_amount(50)
# Payment.pending.find_last_by_amount(100)
#
- # The same dynamic finder style can be used to create the object if it doesn't already exist.
- # This dynamic finder is called with <tt>find_or_create_by_</tt> and will return the object if
- # it already exists and otherwise creates it, then returns it. Protected attributes won't be set
- # unless they are given in a block.
+ # The same dynamic finder style can be used to create the object if it doesn't already exist.
+ # This dynamic finder is called with <tt>find_or_create_by_</tt> and will return the object if
+ # it already exists and otherwise creates it, then returns it. Protected attributes won't be set
+ # unless they are given in a block.
#
# # No 'Summer' tag exists
# Tag.find_or_create_by_name("Summer") # equal to Tag.create(:name => "Summer")
@@ -200,33 +201,33 @@ module ActiveRecord #:nodoc:
# # Now 'Bob' exist and is an 'admin'
# User.find_or_create_by_name('Bob', :age => 40) { |u| u.admin = true }
#
- # Use the <tt>find_or_initialize_by_</tt> finder if you want to return a new record without
+ # Use the <tt>find_or_initialize_by_</tt> finder if you want to return a new record without
# saving it first. Protected attributes won't be set unless they are given in a block.
#
# # No 'Winter' tag exists
# winter = Tag.find_or_initialize_by_name("Winter")
- # winter.new_record? # true
+ # winter.persisted? # false
#
# To find by a subset of the attributes to be used for instantiating a new object, pass a hash instead of
# a list of parameters.
#
# Tag.find_or_create_by_name(:name => "rails", :creator => current_user)
#
- # That will either find an existing tag named "rails", or create a new one while setting the
+ # That will either find an existing tag named "rails", or create a new one while setting the
# user that created it.
#
# Just like <tt>find_by_*</tt>, you can also use <tt>scoped_by_*</tt> to retrieve data. The good thing about
# using this feature is that the very first time result is returned using <tt>method_missing</tt> technique
# but after that the method is declared on the class. Henceforth <tt>method_missing</tt> will not be hit.
#
- # User.scoped_by_user_name('David')
+ # User.scoped_by_user_name('David')
#
# == Saving arrays, hashes, and other non-mappable objects in text columns
#
- # Active Record can serialize any object in text columns using YAML. To do so, you must
+ # Active Record can serialize any object in text columns using YAML. To do so, you must
# specify this with a call to the class method +serialize+.
- # This makes it possible to store arrays, hashes, and other non-mappable objects without doing
- # any additional work.
+ # This makes it possible to store arrays, hashes, and other non-mappable objects without doing
+ # any additional work.
#
# class User < ActiveRecord::Base
# serialize :preferences
@@ -235,7 +236,7 @@ module ActiveRecord #:nodoc:
# user = User.create(:preferences => { "background" => "black", "display" => large })
# User.find(user.id).preferences # => { "background" => "black", "display" => large }
#
- # You can also specify a class option as the second parameter that'll raise an exception
+ # You can also specify a class option as the second parameter that'll raise an exception
# if a serialized object is retrieved as a descendant of a class not in the hierarchy.
#
# class User < ActiveRecord::Base
@@ -245,10 +246,21 @@ module ActiveRecord #:nodoc:
# user = User.create(:preferences => %w( one two three ))
# User.find(user.id).preferences # raises SerializationTypeMismatch
#
+ # When you specify a class option, the default value for that attribute will be a new
+ # instance of that class.
+ #
+ # class User < ActiveRecord::Base
+ # serialize :preferences, OpenStruct
+ # end
+ #
+ # user = User.new
+ # user.preferences.theme_color = "red"
+ #
+ #
# == Single table inheritance
#
- # Active Record allows inheritance by storing the name of the class in a column that by
- # default is named "type" (can be changed by overwriting <tt>Base.inheritance_column</tt>).
+ # Active Record allows inheritance by storing the name of the class in a column that by
+ # default is named "type" (can be changed by overwriting <tt>Base.inheritance_column</tt>).
# This means that an inheritance looking like this:
#
# class Company < ActiveRecord::Base; end
@@ -256,12 +268,12 @@ module ActiveRecord #:nodoc:
# class Client < Company; end
# class PriorityClient < Client; end
#
- # When you do <tt>Firm.create(:name => "37signals")</tt>, this record will be saved in
- # the companies table with type = "Firm". You can then fetch this row again using
+ # When you do <tt>Firm.create(:name => "37signals")</tt>, this record will be saved in
+ # the companies table with type = "Firm". You can then fetch this row again using
# <tt>Company.where(:name => '37signals').first</tt> and it will return a Firm object.
#
- # If you don't have a type column defined in your table, single-table inheritance won't
- # be triggered. In that case, it'll work just like normal subclasses with no special magic
+ # If you don't have a type column defined in your table, single-table inheritance won't
+ # be triggered. In that case, it'll work just like normal subclasses with no special magic
# for differentiating between them or reloading the right type with find.
#
# Note, all the attributes for all the cases are kept in the same table. Read more:
@@ -269,14 +281,14 @@ module ActiveRecord #:nodoc:
#
# == Connection to multiple databases in different models
#
- # Connections are usually created through ActiveRecord::Base.establish_connection and retrieved
- # by ActiveRecord::Base.connection. All classes inheriting from ActiveRecord::Base will use this
- # connection. But you can also set a class-specific connection. For example, if Course is an
+ # Connections are usually created through ActiveRecord::Base.establish_connection and retrieved
+ # by ActiveRecord::Base.connection. All classes inheriting from ActiveRecord::Base will use this
+ # connection. But you can also set a class-specific connection. For example, if Course is an
# ActiveRecord::Base, but resides in a different database, you can just say <tt>Course.establish_connection</tt>
# and Course and all of its subclasses will use this connection instead.
#
- # This feature is implemented by keeping a connection pool in ActiveRecord::Base that is
- # a Hash indexed by the class. If a connection is requested, the retrieve_connection method
+ # This feature is implemented by keeping a connection pool in ActiveRecord::Base that is
+ # a Hash indexed by the class. If a connection is requested, the retrieve_connection method
# will go up the class-hierarchy until a connection is found in the connection pool.
#
# == Exceptions
@@ -284,25 +296,25 @@ module ActiveRecord #:nodoc:
# * ActiveRecordError - Generic error class and superclass of all other errors raised by Active Record.
# * AdapterNotSpecified - The configuration hash used in <tt>establish_connection</tt> didn't include an
# <tt>:adapter</tt> key.
- # * AdapterNotFound - The <tt>:adapter</tt> key used in <tt>establish_connection</tt> specified a
+ # * AdapterNotFound - The <tt>:adapter</tt> key used in <tt>establish_connection</tt> specified a
# non-existent adapter
# (or a bad spelling of an existing one).
- # * AssociationTypeMismatch - The object assigned to the association wasn't of the type
+ # * AssociationTypeMismatch - The object assigned to the association wasn't of the type
# specified in the association definition.
# * SerializationTypeMismatch - The serialized object wasn't of the class specified as the second parameter.
- # * ConnectionNotEstablished+ - No connection has been established. Use <tt>establish_connection</tt>
+ # * ConnectionNotEstablished+ - No connection has been established. Use <tt>establish_connection</tt>
# before querying.
# * RecordNotFound - No record responded to the +find+ method. Either the row with the given ID doesn't exist
# or the row didn't meet the additional restrictions. Some +find+ calls do not raise this exception to signal
# nothing was found, please check its documentation for further details.
# * StatementInvalid - The database server rejected the SQL statement. The precise error is added in the message.
# * MultiparameterAssignmentErrors - Collection of errors that occurred during a mass assignment using the
- # <tt>attributes=</tt> method. The +errors+ property of this exception contains an array of
+ # <tt>attributes=</tt> method. The +errors+ property of this exception contains an array of
# AttributeAssignmentError
# objects that should be inspected to determine which attributes triggered the errors.
- # * AttributeAssignmentError - An error occurred while doing a mass assignment through the
+ # * AttributeAssignmentError - An error occurred while doing a mass assignment through the
# <tt>attributes=</tt> method.
- # You can inspect the +attribute+ property of the exception object to determine which attribute
+ # You can inspect the +attribute+ property of the exception object to determine which attribute
# triggered the error.
#
# *Note*: The attributes listed are class-level attributes (accessible from both the class and instance level).
@@ -311,23 +323,11 @@ module ActiveRecord #:nodoc:
class Base
##
# :singleton-method:
- # Accepts a logger conforming to the interface of Log4r or the default Ruby 1.8+ Logger class,
- # which is then passed on to any new database connections made and which can be retrieved on both
+ # Accepts a logger conforming to the interface of Log4r or the default Ruby 1.8+ Logger class,
+ # which is then passed on to any new database connections made and which can be retrieved on both
# a class and instance level by calling +logger+.
cattr_accessor :logger, :instance_writer => false
- class << self
- def reset_subclasses #:nodoc:
- ActiveSupport::Deprecation.warn 'ActiveRecord::Base.reset_subclasses no longer does anything in Rails 3. It will be removed in the final release; please update your apps and plugins.', caller
- end
-
- def subclasses
- descendants
- end
-
- deprecate :subclasses => :descendants
- end
-
##
# :singleton-method:
# Contains the database configuration - as is typically stored in config/database.yml -
@@ -360,9 +360,9 @@ module ActiveRecord #:nodoc:
##
# :singleton-method:
- # Accessor for the prefix type that will be prepended to every primary key column name.
- # The options are :table_name and :table_name_with_underscore. If the first is specified,
- # the Product class will look for "productid" instead of "id" as the primary column. If the
+ # Accessor for the prefix type that will be prepended to every primary key column name.
+ # The options are :table_name and :table_name_with_underscore. If the first is specified,
+ # the Product class will look for "productid" instead of "id" as the primary column. If the
# latter is specified, the Product class will look for "product_id" instead of "id". Remember
# that this is a global setting for all Active Records.
cattr_accessor :primary_key_prefix_type, :instance_writer => false
@@ -370,13 +370,13 @@ module ActiveRecord #:nodoc:
##
# :singleton-method:
- # Accessor for the name of the prefix string to prepend to every table name. So if set
- # to "basecamp_", all table names will be named like "basecamp_projects", "basecamp_people",
- # etc. This is a convenient way of creating a namespace for tables in a shared database.
+ # Accessor for the name of the prefix string to prepend to every table name. So if set
+ # to "basecamp_", all table names will be named like "basecamp_projects", "basecamp_people",
+ # etc. This is a convenient way of creating a namespace for tables in a shared database.
# By default, the prefix is the empty string.
#
- # If you are organising your models within modules you can add a prefix to the models within
- # a namespace by defining a singleton method in the parent module called table_name_prefix which
+ # If you are organising your models within modules you can add a prefix to the models within
+ # a namespace by defining a singleton method in the parent module called table_name_prefix which
# returns your chosen prefix.
class_attribute :table_name_prefix, :instance_writer => false
self.table_name_prefix = ""
@@ -398,7 +398,7 @@ module ActiveRecord #:nodoc:
##
# :singleton-method:
- # Determines whether to use Time.local (using :local) or Time.utc (using :utc) when pulling
+ # Determines whether to use Time.local (using :local) or Time.utc (using :utc) when pulling
# dates and times from the database. This is set to :local by default.
cattr_accessor :default_timezone, :instance_writer => false
@@default_timezone = :local
@@ -421,24 +421,25 @@ module ActiveRecord #:nodoc:
@@timestamped_migrations = true
# Determine whether to store the full constant name including namespace when using STI
- superclass_delegating_accessor :store_full_sti_class
+ class_attribute :store_full_sti_class
self.store_full_sti_class = true
# Stores the default scope for the class
- class_inheritable_accessor :default_scoping, :instance_writer => false
+ class_attribute :default_scoping, :instance_writer => false
self.default_scoping = []
- class << self # Class methods
- def colorize_logging(*args)
- ActiveSupport::Deprecation.warn "ActiveRecord::Base.colorize_logging and " <<
- "config.active_record.colorize_logging are deprecated. Please use " <<
- "Rails::LogSubscriber.colorize_logging or config.colorize_logging instead", caller
- end
- alias :colorize_logging= :colorize_logging
+ # Returns a hash of all the attributes that have been specified for serialization as
+ # keys and their class restriction as values.
+ class_attribute :serialized_attributes
+ self.serialized_attributes = {}
+
+ class_attribute :_attr_readonly, :instance_writer => false
+ self._attr_readonly = []
+ class << self # Class methods
delegate :find, :first, :last, :all, :destroy, :destroy_all, :exists?, :delete, :delete_all, :update, :update_all, :to => :scoped
delegate :find_each, :find_in_batches, :to => :scoped
- delegate :select, :group, :order, :reorder, :limit, :joins, :where, :preload, :eager_load, :includes, :from, :lock, :readonly, :having, :create_with, :to => :scoped
+ delegate :select, :group, :order, :except, :limit, :offset, :joins, :where, :preload, :eager_load, :includes, :from, :lock, :readonly, :having, :create_with, :to => :scoped
delegate :count, :average, :minimum, :maximum, :sum, :calculate, :to => :scoped
# Executes a custom SQL query against your database and returns all the results. The results will
@@ -462,9 +463,9 @@ module ActiveRecord #:nodoc:
#
# # You can use the same string replacement techniques as you can with ActiveRecord#find
# Post.find_by_sql ["SELECT title FROM posts WHERE author = ? AND created > ?", author_id, start_date]
- # > [#<Post:0x36bff9c @attributes={"first_name"=>"The Cheap Man Buys Twice"}>, ...]
- def find_by_sql(sql)
- connection.select_all(sanitize_sql(sql), "#{name} Load").collect! { |record| instantiate(record) }
+ # > [#<Post:0x36bff9c @attributes={"title"=>"The Cheap Man Buys Twice"}>, ...]
+ def find_by_sql(sql, binds = [])
+ connection.select_all(sanitize_sql(sql), "#{name} Load", binds).collect! { |record| instantiate(record) }
end
# Creates an object (or multiple objects) and saves it to the database, if validations pass.
@@ -519,12 +520,12 @@ module ActiveRecord #:nodoc:
# Attributes listed as readonly will be used to create a new record but update operations will
# ignore these fields.
def attr_readonly(*attributes)
- write_inheritable_attribute(:attr_readonly, Set.new(attributes.map(&:to_s)) + (readonly_attributes || []))
+ self._attr_readonly = Set.new(attributes.map { |a| a.to_s }) + (self._attr_readonly || [])
end
# Returns an array of all the attributes that have been specified as readonly.
def readonly_attributes
- read_inheritable_attribute(:attr_readonly) || []
+ self._attr_readonly
end
# If you have an attribute that needs to be saved to the database as an object, and retrieved as the same object,
@@ -539,24 +540,26 @@ module ActiveRecord #:nodoc:
#
# ==== Example
# # Serialize a preferences attribute
- # class User
+ # class User < ActiveRecord::Base
# serialize :preferences
# end
def serialize(attr_name, class_name = Object)
- serialized_attributes[attr_name.to_s] = class_name
- end
+ coder = if [:load, :dump].all? { |x| class_name.respond_to?(x) }
+ class_name
+ else
+ Coders::YAMLColumn.new(class_name)
+ end
- # Returns a hash of all the attributes that have been specified for serialization as
- # keys and their class restriction as values.
- def serialized_attributes
- read_inheritable_attribute(:attr_serialized) or write_inheritable_attribute(:attr_serialized, {})
+ # merge new serialized attribute and create new hash to ensure that each class in inheritance hierarchy
+ # has its own hash of own serialized attributes
+ self.serialized_attributes = serialized_attributes.merge(attr_name.to_s => coder)
end
- # Guesses the table name (in forced lower-case) based on the name of the class in the
- # inheritance hierarchy descending directly from ActiveRecord::Base. So if the hierarchy
+ # 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 in Active Support, which knows almost all common
+ # to guess the table name even when called on Reply. The rules used to do the guess
+ # are handled by the Inflector class in Active Support, which knows almost all common
# English inflections. You can add new inflections in config/initializers/inflections.rb.
#
# Nested classes are given table names prefixed by the singular form of
@@ -605,10 +608,10 @@ module ActiveRecord #:nodoc:
(parents.detect{ |p| p.respond_to?(:table_name_prefix) } || self).table_name_prefix
end
- # Defines the column name for use with single table inheritance. Use
+ # Defines the column name for use with single table inheritance. Use
# <tt>set_inheritance_column</tt> to set a different value.
def inheritance_column
- @inheritance_column ||= "type".freeze
+ @inheritance_column ||= "type"
end
# Lazy-set the sequence name to the connection's default. This method
@@ -623,7 +626,7 @@ module ActiveRecord #:nodoc:
default
end
- # Sets the table name. If the value is nil or false then the value returned by the given
+ # Sets the table name. If the value is nil or false then the value returned by the given
# block is used.
#
# class Project < ActiveRecord::Base
@@ -632,6 +635,9 @@ module ActiveRecord #:nodoc:
def set_table_name(value = nil, &block)
@quoted_table_name = nil
define_attr_method :table_name, value, &block
+
+ @arel_table = Arel::Table.new(table_name, arel_engine)
+ @relation = Relation.new(self, arel_table)
end
alias :table_name= :set_table_name
@@ -675,16 +681,12 @@ module ActiveRecord #:nodoc:
# Returns an array of column objects for the table associated with this class.
def columns
- unless defined?(@columns) && @columns
- @columns = connection.columns(table_name, "#{name} Columns")
- @columns.each { |column| column.primary = column.name == primary_key }
- end
- @columns
+ connection_pool.columns[table_name]
end
# Returns a hash of column objects for the table associated with this class.
def columns_hash
- @columns_hash ||= columns.inject({}) { |hash, column| hash[column.name] = column; hash }
+ connection_pool.columns_hash[table_name]
end
# Returns an array of column names as strings.
@@ -739,13 +741,16 @@ module ActiveRecord #:nodoc:
# end
# end
def reset_column_information
+ connection.clear_cache!
undefine_attribute_methods
- @column_names = @columns = @columns_hash = @content_columns = @dynamic_methods_hash = @inheritance_column = nil
- @arel_engine = @relation = @arel_table = nil
+ connection_pool.clear_table_cache!(table_name) if table_exists?
+
+ @column_names = @content_columns = @dynamic_methods_hash = @inheritance_column = nil
+ @arel_engine = @relation = nil
end
- def reset_column_information_and_inheritable_attributes_for_all_subclasses#:nodoc:
- descendants.each { |klass| klass.reset_inheritable_attributes; klass.reset_column_information }
+ def clear_cache! # :nodoc:
+ connection_pool.clear_cache!
end
def attribute_method?(attribute)
@@ -756,15 +761,12 @@ module ActiveRecord #:nodoc:
def lookup_ancestors #:nodoc:
klass = self
classes = [klass]
+ return classes if klass == ActiveRecord::Base
+
while klass != klass.base_class
classes << klass = klass.superclass
end
classes
- rescue
- # OPTIMIZE this rescue is to fix this test: ./test/cases/reflection_test.rb:56:in `test_human_name_for_column'
- # Apparently the method base_class causes some trouble.
- # It now works for sure.
- [self]
end
# Set the i18n scope to overwrite ActiveModel.
@@ -786,7 +788,7 @@ module ActiveRecord #:nodoc:
:true == (@finder_needs_type_condition ||= descends_from_active_record? ? :false : :true)
end
- # Returns a string like 'Post id:integer, title:string, body:text'
+ # Returns a string like 'Post(id:integer, title:string, body:text)'
def inspect
if self == Base
super
@@ -814,6 +816,10 @@ module ActiveRecord #:nodoc:
object.is_a?(self)
end
+ def symbolized_base_class
+ @symbolized_base_class ||= base_class.to_s.to_sym
+ end
+
# Returns the base AR subclass that this class descends from. If A
# extends AR::Base, A.base_class will return A. If B descends from A
# through some arbitrarily deep hierarchy, B.base_class will return A.
@@ -847,15 +853,15 @@ module ActiveRecord #:nodoc:
end
def arel_table
- @arel_table ||= Arel::Table.new(table_name, arel_engine)
+ Arel::Table.new(table_name, arel_engine)
end
def arel_engine
@arel_engine ||= begin
if self == ActiveRecord::Base
- Arel::Table.engine
+ ActiveRecord::Base
else
- connection_handler.connection_pools[name] ? Arel::Sql::Engine.new(self) : superclass.arel_engine
+ connection_handler.connection_pools[name] ? self : superclass.arel_engine
end
end
end
@@ -876,7 +882,12 @@ module ActiveRecord #:nodoc:
# limit(10) # Fires "SELECT * FROM posts LIMIT 10"
# }
#
- def unscoped
+ # It is recommended to use block form of unscoped because chaining unscoped with <tt>scope</tt>
+ # does not work. Assuming that <tt>published</tt> is a <tt>scope</tt> following two statements are same.
+ #
+ # Post.unscoped.published
+ # Post.published
+ def unscoped #:nodoc:
block_given? ? relation.scoping { yield } : relation
end
@@ -885,32 +896,55 @@ module ActiveRecord #:nodoc:
Thread.current[key] = Thread.current[key].presence || self.default_scoping.dup
end
- private
+ def before_remove_const #:nodoc:
+ reset_scoped_methods
+ end
- def relation #:nodoc:
- @relation ||= Relation.new(self, arel_table)
- finder_needs_type_condition? ? @relation.where(type_condition) : @relation
+ # Specifies how the record is loaded by +Marshal+.
+ #
+ # +_load+ sets an instance variable for each key in the hash it takes as input.
+ # Override this method if you require more complex marshalling.
+ def _load(data)
+ record = allocate
+ record.init_with(Marshal.load(data))
+ record
+ end
+
+
+ # Finder methods must instantiate through this method to work with the
+ # single-table inheritance model that makes it possible to create
+ # objects of different types from the same table.
+ def instantiate(record)
+ sti_class = find_sti_class(record[inheritance_column])
+ record_id = sti_class.primary_key && record[sti_class.primary_key]
+
+ if ActiveRecord::IdentityMap.enabled? && record_id
+ if (column = sti_class.columns_hash[sti_class.primary_key]) && column.number?
+ record_id = record_id.to_i
+ end
+ if instance = IdentityMap.get(sti_class, record_id)
+ instance.reinit_with('attributes' => record)
+ else
+ instance = sti_class.allocate.init_with('attributes' => record)
+ IdentityMap.add(instance)
+ end
+ else
+ instance = sti_class.allocate.init_with('attributes' => record)
end
- # Finder methods must instantiate through this method to work with the
- # single-table inheritance model that makes it possible to create
- # objects of different types from the same table.
- def instantiate(record)
- object = find_sti_class(record[inheritance_column]).allocate
+ instance
+ end
- object.instance_variable_set(:@attributes, record)
- object.instance_variable_set(:@attributes_cache, {})
- object.instance_variable_set(:@new_record, false)
- object.instance_variable_set(:@readonly, false)
- object.instance_variable_set(:@destroyed, false)
- object.instance_variable_set(:@marked_for_destruction, false)
- object.instance_variable_set(:@previously_changed, {})
- object.instance_variable_set(:@changed_attributes, {})
+ private
- object.send(:_run_find_callbacks)
- object.send(:_run_initialize_callbacks)
+ def relation #:nodoc:
+ @relation ||= Relation.new(self, arel_table)
- object
+ if finder_needs_type_condition?
+ @relation.where(type_condition).create_with(inheritance_column.to_sym => sti_name)
+ else
+ @relation
+ end
end
def find_sti_class(type_name)
@@ -918,7 +952,11 @@ module ActiveRecord #:nodoc:
self
else
begin
- compute_type(type_name)
+ if store_full_sti_class
+ ActiveSupport::Dependencies.constantize(type_name)
+ else
+ compute_type(type_name)
+ end
rescue NameError
raise SubclassNotFound,
"The single-table inheritance mechanism failed to locate the subclass: '#{type_name}'. " +
@@ -930,17 +968,16 @@ module ActiveRecord #:nodoc:
end
def construct_finder_arel(options = {}, scope = nil)
- relation = options.is_a?(Hash) ? unscoped.apply_finder_options(options) : unscoped.merge(options)
+ relation = options.is_a?(Hash) ? unscoped.apply_finder_options(options) : options
relation = scope.merge(relation) if scope
relation
end
- def type_condition
- sti_column = arel_table[inheritance_column]
- condition = sti_column.eq(sti_name)
- descendants.each { |subclass| condition = condition.or(sti_column.eq(subclass.sti_name)) }
+ def type_condition(table = arel_table)
+ sti_column = table[inheritance_column.to_sym]
+ sti_names = ([self] + descendants).map { |model| model.sti_name }
- condition
+ sti_column.in(sti_names)
end
# Guesses the table name, but does not decorate it with prefix and suffix information.
@@ -958,7 +995,7 @@ module ActiveRecord #:nodoc:
if parent < ActiveRecord::Base && !parent.abstract_class?
contained = parent.table_name
contained = contained.singularize if parent.pluralize_table_names
- contained << '_'
+ contained += '_'
end
"#{full_table_name_prefix}#{contained}#{undecorated_table_name(name)}#{table_name_suffix}"
else
@@ -967,14 +1004,14 @@ module ActiveRecord #:nodoc:
end
end
- # Enables dynamic finders like <tt>User.find_by_user_name(user_name)</tt> and
+ # Enables dynamic finders like <tt>User.find_by_user_name(user_name)</tt> and
# <tt>User.scoped_by_user_name(user_name). Refer to Dynamic attribute-based finders
# section at the top of this file for more detailed information.
#
- # It's even possible to use all the additional parameters to +find+. For example, the
+ # It's even possible to use all the additional parameters to +find+. For example, the
# full interface for +find_all_by_amount+ is actually <tt>find_all_by_amount(amount, options)</tt>.
#
- # Each dynamic finder using <tt>scoped_by_*</tt> is also defined in the class after it
+ # Each dynamic finder using <tt>scoped_by_*</tt> is also defined in the class after it
# is first invoked, so that future attempts to use it do not run through method_missing.
def method_missing(method_id, *arguments, &block)
if match = DynamicFinderMatch.match(method_id)
@@ -992,14 +1029,11 @@ module ActiveRecord #:nodoc:
super unless all_attributes_exists?(attribute_names)
if match.scope?
self.class_eval <<-METHOD, __FILE__, __LINE__ + 1
- def self.#{method_id}(*args) # def self.scoped_by_user_name_and_password(*args)
- options = args.extract_options! # options = args.extract_options!
- attributes = construct_attributes_from_arguments( # attributes = construct_attributes_from_arguments(
- [:#{attribute_names.join(',:')}], args # [:user_name, :password], args
- ) # )
- #
- scoped(:conditions => attributes) # scoped(:conditions => attributes)
- end # end
+ def self.#{method_id}(*args) # def self.scoped_by_user_name_and_password(*args)
+ attributes = Hash[[:#{attribute_names.join(',:')}].zip(args)] # attributes = Hash[[:user_name, :password].zip(args)]
+ #
+ scoped(:conditions => attributes) # scoped(:conditions => attributes)
+ end # end
METHOD
send(method_id, *arguments)
end
@@ -1008,30 +1042,22 @@ module ActiveRecord #:nodoc:
end
end
- def construct_attributes_from_arguments(attribute_names, arguments)
- attributes = {}
- attribute_names.each_with_index { |name, idx| attributes[name] = arguments[idx] }
- attributes
- end
-
# Similar in purpose to +expand_hash_conditions_for_aggregates+.
def expand_attribute_names_for_aggregates(attribute_names)
- expanded_attribute_names = []
- attribute_names.each do |attribute_name|
+ attribute_names.map { |attribute_name|
unless (aggregation = reflect_on_aggregation(attribute_name.to_sym)).nil?
- aggregate_mapping(aggregation).each do |field_attr, aggregate_attr|
- expanded_attribute_names << field_attr
+ aggregate_mapping(aggregation).map do |field_attr, _|
+ field_attr.to_sym
end
else
- expanded_attribute_names << attribute_name
+ attribute_name.to_sym
end
- end
- expanded_attribute_names
+ }.flatten
end
def all_attributes_exists?(attribute_names)
- attribute_names = expand_attribute_names_for_aggregates(attribute_names)
- attribute_names.all? { |name| column_methods_hash.include?(name.to_sym) }
+ (expand_attribute_names_for_aggregates(attribute_names) -
+ column_methods_hash.keys).empty?
end
protected
@@ -1087,9 +1113,9 @@ module ActiveRecord #:nodoc:
if method_scoping.is_a?(Hash)
# Dup first and second level of hash (method and params).
- method_scoping = method_scoping.inject({}) do |hash, (method, params)|
- hash[method] = (params == true) ? params : params.dup
- hash
+ method_scoping = method_scoping.dup
+ method_scoping.each do |method, params|
+ method_scoping[method] = params.dup unless params == true
end
method_scoping.assert_valid_keys([ :find, :create ])
@@ -1147,12 +1173,33 @@ MSG
# class Person < ActiveRecord::Base
# default_scope order('last_name, first_name')
# end
+ #
+ # <tt>default_scope</tt> is also applied while creating/building a record. It is not
+ # applied while updating a record.
+ #
+ # class Article < ActiveRecord::Base
+ # default_scope where(:published => true)
+ # end
+ #
+ # Article.new.published # => true
+ # Article.create.published # => true
def default_scope(options = {})
- self.default_scoping << construct_finder_arel(options, default_scoping.pop)
+ reset_scoped_methods
+ default_scoping = self.default_scoping.dup
+ self.default_scoping = default_scoping << construct_finder_arel(options, default_scoping.pop)
end
def current_scoped_methods #:nodoc:
- scoped_methods.last
+ method = scoped_methods.last
+ if method.respond_to?(:call)
+ relation.scoping { method.call }
+ else
+ method
+ end
+ end
+
+ def reset_scoped_methods #:nodoc:
+ Thread.current[:"#{self}_scoped_methods"] = nil
end
# Returns the class type of the record using the current module as a prefix. So descendants of
@@ -1161,7 +1208,7 @@ MSG
if type_name.match(/^::/)
# If the type is prefixed with a scope operator then we assume that
# the type_name is an absolute reference.
- type_name.constantize
+ ActiveSupport::Dependencies.constantize(type_name)
else
# Build a list of candidates to search for
candidates = []
@@ -1170,7 +1217,7 @@ MSG
candidates.each do |candidate|
begin
- constant = candidate.constantize
+ constant = ActiveSupport::Dependencies.constantize(candidate)
return constant if candidate == constant.to_s
rescue NameError => e
# We don't want to swallow NoMethodError < NameError errors
@@ -1274,9 +1321,11 @@ MSG
def sanitize_sql_hash_for_conditions(attrs, default_table_name = self.table_name)
attrs = expand_hash_conditions_for_aggregates(attrs)
- table = Arel::Table.new(self.table_name, :engine => arel_engine, :as => default_table_name)
- builder = PredicateBuilder.new(arel_engine)
- builder.build_from_hash(attrs, table).map(&:to_sql).join(' AND ')
+ table = Arel::Table.new(table_name).alias(default_table_name)
+ viz = Arel::Visitors.for(arel_engine)
+ PredicateBuilder.build_from_hash(arel_engine, attrs, table).map { |b|
+ viz.accept b
+ }.join(' AND ')
end
alias_method :sanitize_sql_hash, :sanitize_sql_hash_for_conditions
@@ -1294,7 +1343,7 @@ MSG
# ["name='%s' and group_id='%s'", "foo'bar", 4] returns "name='foo''bar' and group_id='4'"
def sanitize_sql_array(ary)
statement, *values = ary
- if values.first.is_a?(Hash) and statement =~ /:\w+/
+ if values.first.is_a?(Hash) && statement =~ /:\w+/
replace_named_bind_variables(statement, values.first)
elsif statement.include?('?')
replace_bind_variables(statement, values)
@@ -1375,6 +1424,8 @@ MSG
# hence you can't have attributes that aren't part of the table columns.
def initialize(attributes = nil)
@attributes = attributes_from_column_definition
+ @association_cache = {}
+ @aggregation_cache = {}
@attributes_cache = {}
@new_record = true
@readonly = false
@@ -1384,43 +1435,66 @@ MSG
@changed_attributes = {}
ensure_proper_type
+ set_serialized_attributes
- if scope = self.class.send(:current_scoped_methods)
- create_with = scope.scope_for_create
- create_with.each { |att,value| self.send("#{att}=", value) } if create_with
- end
+ populate_with_current_scope_attributes
self.attributes = attributes unless attributes.nil?
result = yield self if block_given?
- _run_initialize_callbacks
+ run_callbacks :initialize
result
end
- # Cloned objects have no id assigned and are treated as new records. Note that this is a "shallow" clone
- # as it copies the object's attributes only, not its associations. The extent of a "deep" clone is
- # application specific and is therefore left to the application to implement according to its need.
- def initialize_copy(other)
- _run_after_initialize_callbacks if respond_to?(:_run_after_initialize_callbacks)
- cloned_attributes = other.clone_attributes(:read_attribute_before_type_cast)
- cloned_attributes.delete(self.class.primary_key)
+ # Populate +coder+ with attributes about this record that should be
+ # serialized. The structure of +coder+ defined in this method is
+ # guaranteed to match the structure of +coder+ passed to the +init_with+
+ # method.
+ #
+ # Example:
+ #
+ # class Post < ActiveRecord::Base
+ # end
+ # coder = {}
+ # Post.new.encode_with(coder)
+ # coder # => { 'id' => nil, ... }
+ def encode_with(coder)
+ coder['attributes'] = attributes
+ end
- @attributes = cloned_attributes
+ # Initialize an empty model object from +coder+. +coder+ must contain
+ # the attributes necessary for initializing an empty model object. For
+ # example:
+ #
+ # class Post < ActiveRecord::Base
+ # end
+ #
+ # post = Post.allocate
+ # post.init_with('attributes' => { 'title' => 'hello world' })
+ # post.title # => 'hello world'
+ def init_with(coder)
+ @attributes = coder['attributes']
- @changed_attributes = {}
- attributes_from_column_definition.each do |attr, orig_value|
- @changed_attributes[attr] = orig_value if field_changed?(attr, orig_value, @attributes[attr])
- end
+ set_serialized_attributes
- clear_aggregation_cache
- clear_association_cache
- @attributes_cache = {}
- @new_record = true
- ensure_proper_type
+ @attributes_cache, @previously_changed, @changed_attributes = {}, {}, {}
+ @association_cache = {}
+ @aggregation_cache = {}
+ @readonly = @destroyed = @marked_for_destruction = false
+ @new_record = false
+ run_callbacks :find
+ run_callbacks :initialize
- if scope = self.class.send(:current_scoped_methods)
- create_with = scope.scope_for_create
- create_with.each { |att,value| self.send("#{att}=", value) } if create_with
- end
+ self
+ end
+
+ # Specifies how the record is dumped by +Marshal+.
+ #
+ # +_dump+ emits a marshalled hash which has been passed to +encode_with+. Override this
+ # method if you require more complex marshalling.
+ def _dump(level)
+ dump = {}
+ encode_with(dump)
+ Marshal.dump(dump)
end
# Returns a String, which Action Pack uses for constructing an URL to this
@@ -1477,22 +1551,9 @@ MSG
@attributes.has_key?(attr_name.to_s)
end
- # Returns an array of names for the attributes available on this object sorted alphabetically.
+ # Returns an array of names for the attributes available on this object.
def attribute_names
- @attributes.keys.sort
- end
-
- # Returns the value of the attribute identified by <tt>attr_name</tt> after it has been typecast (for example,
- # "2004-12-12" in a data column is cast to a date object, like Date.new(2004, 12, 12)).
- # (Alias for the protected read_attribute method).
- def [](attr_name)
- read_attribute(attr_name)
- end
-
- # Updates the attribute identified by <tt>attr_name</tt> with the specified +value+.
- # (Alias for the protected write_attribute method).
- def []=(attr_name, value)
- write_attribute(attr_name, value)
+ @attributes.keys
end
# Allows you to set all the attributes at once by passing in a hash with keys
@@ -1525,8 +1586,10 @@ MSG
attributes.each do |k, v|
if k.include?("(")
multi_parameter_attributes << [ k, v ]
+ elsif respond_to?("#{k}=")
+ send("#{k}=", v)
else
- respond_to?(:"#{k}=") ? send(:"#{k}=", v) : raise(UnknownAttributeError, "unknown attribute: #{k}")
+ raise(UnknownAttributeError, "unknown attribute: #{k}")
end
end
@@ -1535,9 +1598,7 @@ MSG
# Returns a hash of all the attributes with their names as keys and the values of the attributes as values.
def attributes
- attrs = {}
- attribute_names.each { |name| attrs[name] = read_attribute(name) }
- attrs
+ Hash[@attributes.map { |name, _| [name, read_attribute(name)] }]
end
# Returns an <tt>#inspect</tt>-like string for the value of the
@@ -1568,8 +1629,7 @@ MSG
# Returns true if the specified +attribute+ has been set by the user or by a database load and is neither
# nil nor empty? (the latter only applies to objects that respond to empty?, most notably Strings).
def attribute_present?(attribute)
- value = read_attribute(attribute)
- !value.blank?
+ !_read_attribute(attribute).blank?
end
# Returns the column object for the named attribute.
@@ -1577,16 +1637,25 @@ MSG
self.class.columns_hash[name.to_s]
end
- # Returns true if the +comparison_object+ is the same object, or is of the same type and has the same id.
+ # Returns true if +comparison_object+ is the same exact object, or +comparison_object+
+ # is of the same type and +self+ has an ID and it is equal to +comparison_object.id+.
+ #
+ # Note that new records are different from any other record by definition, unless the
+ # other record is the receiver itself. Besides, if you fetch existing records with
+ # +select+ and leave the ID out, you're on your own, this predicate will return false.
+ #
+ # Note also that destroying a record preserves its ID in the model instance, so deleted
+ # models are still comparable.
def ==(comparison_object)
comparison_object.equal?(self) ||
- (comparison_object.instance_of?(self.class) &&
- comparison_object.id == id && !comparison_object.new_record?)
+ comparison_object.instance_of?(self.class) &&
+ id.present? &&
+ comparison_object.id == id
end
# Delegates to ==
def eql?(comparison_object)
- self == (comparison_object)
+ self == comparison_object
end
# Delegates to id in order to allow two records of the same type and id to work with something like:
@@ -1605,11 +1674,42 @@ MSG
@attributes.frozen?
end
- # Returns duplicated record with unfreezed attributes.
- def dup
- obj = super
- obj.instance_variable_set('@attributes', @attributes.dup)
- obj
+ # Backport dup from 1.9 so that initialize_dup() gets called
+ unless Object.respond_to?(:initialize_dup)
+ def dup # :nodoc:
+ copy = super
+ copy.initialize_dup(self)
+ copy
+ end
+ end
+
+ # Duped objects have no id assigned and are treated as new records. Note
+ # that this is a "shallow" copy as it copies the object's attributes
+ # only, not its associations. The extent of a "deep" copy is application
+ # specific and is therefore left to the application to implement according
+ # to its need.
+ # The dup method does not preserve the timestamps (created|updated)_(at|on).
+ def initialize_dup(other)
+ cloned_attributes = other.clone_attributes(:read_attribute_before_type_cast)
+ cloned_attributes.delete(self.class.primary_key)
+
+ @attributes = cloned_attributes
+
+ _run_after_initialize_callbacks if respond_to?(:_run_after_initialize_callbacks)
+
+ @changed_attributes = {}
+ attributes_from_column_definition.each do |attr, orig_value|
+ @changed_attributes[attr] = orig_value if field_changed?(attr, orig_value, @attributes[attr])
+ end
+
+ @aggregation_cache = {}
+ @association_cache = {}
+ @attributes_cache = {}
+ @new_record = true
+
+ ensure_proper_type
+ populate_with_current_scope_attributes
+ clear_timestamp_attributes
end
# Returns +true+ if the record is read only. Records loaded through joins with piggy-back
@@ -1626,7 +1726,7 @@ MSG
# Returns the contents of the record as a nicely formatted string.
def inspect
attributes_as_nice_string = self.class.column_names.collect { |name|
- if has_attribute?(name) || new_record?
+ if has_attribute?(name)
"#{name}: #{attribute_for_inspect(name)}"
end
}.compact.join(", ")
@@ -1650,10 +1750,17 @@ MSG
private
- # Sets the attribute used for single table inheritance to this class name if this is not the
+ def set_serialized_attributes
+ (@attributes.keys & self.class.serialized_attributes.keys).each do |key|
+ coder = self.class.serialized_attributes[key]
+ @attributes[key] = coder.load @attributes[key]
+ end
+ end
+
+ # Sets the attribute used for single table inheritance to this class name if this is not the
# ActiveRecord::Base descendant.
- # Considering the hierarchy Reply < Message < ActiveRecord::Base, this makes it possible to
- # do Reply.new without having to set <tt>Reply[Reply.inheritance_column] = "Reply"</tt> yourself.
+ # Considering the hierarchy Reply < Message < ActiveRecord::Base, this makes it possible to
+ # do Reply.new without having to set <tt>Reply[Reply.inheritance_column] = "Reply"</tt> yourself.
# No such attribute would be set for objects of the Message class in that example.
def ensure_proper_type
unless self.class.descends_from_active_record?
@@ -1671,17 +1778,25 @@ MSG
# Returns a copy of the attributes hash where all the values have been safely quoted for use in
# an Arel insert/update method.
def arel_attributes_values(include_primary_key = true, include_readonly_attributes = true, attribute_names = @attributes.keys)
- attrs = {}
+ attrs = {}
+ klass = self.class
+ arel_table = klass.arel_table
+
attribute_names.each do |name|
if (column = column_for_attribute(name)) && (include_primary_key || !column.primary)
if include_readonly_attributes || (!include_readonly_attributes && !self.class.readonly_attributes.include?(name))
- value = read_attribute(name)
- if value && ((self.class.serialized_attributes.has_key?(name) && (value.acts_like?(:date) || value.acts_like?(:time))) || value.is_a?(Hash) || value.is_a?(Array))
- value = value.to_yaml
- end
- attrs[self.class.arel_table[name]] = value
+ value = if coder = klass.serialized_attributes[name]
+ coder.dump @attributes[name]
+ else
+ # FIXME: we need @attributes to be used consistently.
+ # If the values stored in @attributes were already type
+ # casted, this code could be simplified
+ read_attribute(name)
+ end
+
+ attrs[arel_table[name]] = value
end
end
end
@@ -1693,18 +1808,12 @@ MSG
self.class.connection.quote(value, column)
end
- # Interpolate custom SQL string in instance context.
- # Optional record argument is meant for custom insert_sql.
- def interpolate_sql(sql, record = nil)
- instance_eval("%@#{sql.gsub('@', '\@')}@", __FILE__, __LINE__)
- end
-
# Instantiates objects for all attribute classes that needs more than one constructor parameter. This is done
# by calling new on the column type or aggregation type (through composed_of) object with these parameters.
# So having the pairs written_on(1) = "2004", written_on(2) = "6", written_on(3) = "24", will instantiate
# written_on (a date type) with Date.new("2004", "6", "24"). You can also specify a typecast character in the
- # parentheses to have the parameters typecasted before they're used in the constructor. Use i for Fixnum,
- # f for Float, s for String, and a for Array. If all the values for a given attribute are empty, the
+ # parentheses to have the parameters typecasted before they're used in the constructor. Use i for Fixnum,
+ # f for Float, s for String, and a for Array. If all the values for a given attribute are empty, the
# attribute will be set to nil.
def assign_multiparameter_attributes(pairs)
execute_callstack_for_multiparameter_attributes(
@@ -1727,7 +1836,7 @@ MSG
klass = (self.class.reflect_on_aggregation(name.to_sym) || column_for_attribute(name)).klass
# in order to allow a date to be set without a year, we must keep the empty values.
# Otherwise, we wouldn't be able to distinguish it from a date with an empty day.
- values = values_with_empty_parameters.reject(&:nil?)
+ values = values_with_empty_parameters.reject { |v| v.nil? }
if values.empty?
send(name + "=", nil)
@@ -1786,10 +1895,7 @@ MSG
end
def quote_columns(quoter, hash)
- hash.inject({}) do |quoted, (name, value)|
- quoted[quoter.quote_column_name(name)] = value
- quoted
- end
+ Hash[hash.map { |name, value| [quoter.quote_column_name(name), value] }]
end
def quoted_comma_pair_list(quoter, hash)
@@ -1808,9 +1914,21 @@ MSG
end
end
- def object_from_yaml(string)
- return string unless string.is_a?(String) && string =~ /^---/
- YAML::load(string) rescue string
+ def populate_with_current_scope_attributes
+ if scope = self.class.send(:current_scoped_methods)
+ create_with = scope.scope_for_create
+ create_with.each { |att,value|
+ respond_to?("#{att}=") && send("#{att}=", value)
+ }
+ end
+ end
+
+ # Clear attributes and changed_attributes
+ def clear_timestamp_attributes
+ all_timestamp_attributes_in_model.each do |attribute_name|
+ self[attribute_name] = nil
+ changed_attributes.delete(attribute_name)
+ end
end
end
@@ -1832,7 +1950,9 @@ MSG
include AttributeMethods::Dirty
include ActiveModel::MassAssignmentSecurity
include Callbacks, ActiveModel::Observing, Timestamp
- include Associations, AssociationPreload, NamedScope
+ include Associations, NamedScope
+ include IdentityMap
+ include ActiveModel::SecurePassword
# AutosaveAssociation needs to be included before Transactions, because we want
# #save_with_autosave_associations to be wrapped inside a transaction.
@@ -1840,6 +1960,17 @@ MSG
include Aggregations, Transactions, Reflection, Serialization
NilClass.add_whiner(self) if NilClass.respond_to?(:add_whiner)
+
+ # Returns the value of the attribute identified by <tt>attr_name</tt> after it has been typecast (for example,
+ # "2004-12-12" in a data column is cast to a date object, like Date.new(2004, 12, 12)).
+ # (Alias for the protected read_attribute method).
+ alias [] read_attribute
+
+ # Updates the attribute identified by <tt>attr_name</tt> with the specified +value+.
+ # (Alias for the protected write_attribute method).
+ alias []= write_attribute
+
+ public :[], :[]=
end
end
diff --git a/activerecord/lib/active_record/callbacks.rb b/activerecord/lib/active_record/callbacks.rb
index aa92bf999f..86d58df99b 100644
--- a/activerecord/lib/active_record/callbacks.rb
+++ b/activerecord/lib/active_record/callbacks.rb
@@ -2,8 +2,8 @@ require 'active_support/core_ext/array/wrap'
module ActiveRecord
# = Active Record Callbacks
- #
- # Callbacks are hooks into the lifecycle of an Active Record object that allow you to trigger logic
+ #
+ # Callbacks are hooks into the life cycle of an Active Record object that allow you to trigger logic
# before or after an alteration of the object state. This can be used to make sure that associated and
# dependent objects are deleted when +destroy+ is called (by overwriting +before_destroy+) or to massage attributes
# before they're validated (by overwriting +before_validation+). As an example of the callbacks initiated, consider
@@ -25,9 +25,13 @@ module ActiveRecord
# Check out <tt>ActiveRecord::Transactions</tt> for more details about <tt>after_commit</tt> and
# <tt>after_rollback</tt>.
#
- # That's a total of ten callbacks, which gives you immense power to react and prepare for each state in the
- # Active Record lifecycle. The sequence for calling <tt>Base#save</tt> for an existing record is similar,
- # except that each <tt>_on_create</tt> callback is replaced by the corresponding <tt>_on_update</tt> callback.
+ # Lastly an <tt>after_find</tt> and <tt>after_initialize</tt> callback is triggered for each object that
+ # is found and instantiated by a finder, with <tt>after_initialize</tt> being triggered after new objects
+ # are instantiated as well.
+ #
+ # That's a total of twelve callbacks, which gives you immense power to react and prepare for each state in the
+ # Active Record life cycle. The sequence for calling <tt>Base#save</tt> for an existing record is similar,
+ # except that each <tt>_create</tt> callback is replaced by the corresponding <tt>_update</tt> callback.
#
# Examples:
# class CreditCard < ActiveRecord::Base
@@ -55,8 +59,8 @@ module ActiveRecord
#
# == Inheritable callback queues
#
- # Besides the overwritable callback methods, it's also possible to register callbacks through the
- # use of the callback macros. Their main advantage is that the macros add behavior into a callback
+ # Besides the overwritable callback methods, it's also possible to register callbacks through the
+ # use of the callback macros. Their main advantage is that the macros add behavior into a callback
# queue that is kept intact down through an inheritance hierarchy.
#
# class Topic < ActiveRecord::Base
@@ -67,9 +71,9 @@ module ActiveRecord
# before_destroy :destroy_readers
# end
#
- # Now, when <tt>Topic#destroy</tt> is run only +destroy_author+ is called. When <tt>Reply#destroy</tt> is
- # run, both +destroy_author+ and +destroy_readers+ are called. Contrast this to the following situation
- # where the +before_destroy+ methis is overriden:
+ # Now, when <tt>Topic#destroy</tt> is run only +destroy_author+ is called. When <tt>Reply#destroy</tt> is
+ # run, both +destroy_author+ and +destroy_readers+ are called. Contrast this to the following situation
+ # where the +before_destroy+ method is overridden:
#
# class Topic < ActiveRecord::Base
# def before_destroy() destroy_author end
@@ -79,20 +83,20 @@ module ActiveRecord
# def before_destroy() destroy_readers end
# end
#
- # In that case, <tt>Reply#destroy</tt> would only run +destroy_readers+ and _not_ +destroy_author+.
- # So, use the callback macros when you want to ensure that a certain callback is called for the entire
- # hierarchy, and use the regular overwriteable methods when you want to leave it up to each descendant
+ # In that case, <tt>Reply#destroy</tt> would only run +destroy_readers+ and _not_ +destroy_author+.
+ # So, use the callback macros when you want to ensure that a certain callback is called for the entire
+ # hierarchy, and use the regular overwriteable methods when you want to leave it up to each descendant
# to decide whether they want to call +super+ and trigger the inherited callbacks.
#
- # *IMPORTANT:* In order for inheritance to work for the callback queues, you must specify the
- # callbacks before specifying the associations. Otherwise, you might trigger the loading of a
+ # *IMPORTANT:* In order for inheritance to work for the callback queues, you must specify the
+ # callbacks before specifying the associations. Otherwise, you might trigger the loading of a
# child before the parent has registered the callbacks and they won't be inherited.
#
# == Types of callbacks
#
# There are four types of callbacks accepted by the callback macros: Method references (symbol), callback objects,
- # inline methods (using a proc), and inline eval methods (using a string). Method references and callback objects
- # are the recommended approaches, inline methods using a proc are sometimes appropriate (such as for
+ # inline methods (using a proc), and inline eval methods (using a string). Method references and callback objects
+ # are the recommended approaches, inline methods using a proc are sometimes appropriate (such as for
# creating mix-ins), and inline eval methods are deprecated.
#
# The method reference callbacks work by specifying a protected or private method available in the object, like this:
@@ -170,14 +174,14 @@ module ActiveRecord
# end
# end
#
- # The callback macros usually accept a symbol for the method they're supposed to run, but you can also
+ # 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:
#
# class Topic < ActiveRecord::Base
# before_destroy 'self.class.delete_all "parent_id = #{id}"'
# end
#
- # Notice that single quotes (') are used so the <tt>#{id}</tt> part isn't evaluated until the callback
+ # Notice that single quotes (') are used so the <tt>#{id}</tt> part isn't evaluated until the callback
# is triggered. Also note that these inline callbacks can be stacked just like the regular ones:
#
# class Topic < ActiveRecord::Base
@@ -185,25 +189,17 @@ module ActiveRecord
# 'puts "Evaluated after parents are destroyed"'
# end
#
- # == The +after_find+ and +after_initialize+ exceptions
- #
- # Because +after_find+ and +after_initialize+ are called for each object found and instantiated by a finder,
- # such as <tt>Base.find(:all)</tt>, we've had to implement a simple performance constraint (50% more speed
- # on a simple test case). Unlike all the other callbacks, +after_find+ and +after_initialize+ will only be
- # run if an explicit implementation is defined (<tt>def after_find</tt>). In that case, all of the
- # callback types will be called.
- #
# == <tt>before_validation*</tt> returning statements
#
- # If the returning value of a +before_validation+ callback can be evaluated to +false+, the process will be
- # aborted and <tt>Base#save</tt> will return +false+. If Base#save! is called it will raise a
+ # If the returning value of a +before_validation+ callback can be evaluated to +false+, the process will be
+ # aborted and <tt>Base#save</tt> will return +false+. If Base#save! is called it will raise a
# ActiveRecord::RecordInvalid exception. Nothing will be appended to the errors object.
#
# == Canceling callbacks
#
- # If a <tt>before_*</tt> callback returns +false+, all the later callbacks and the associated action are
- # cancelled. If an <tt>after_*</tt> callback returns +false+, all the later callbacks are cancelled.
- # Callbacks are generally run in the order they are defined, with the exception of callbacks defined as
+ # If a <tt>before_*</tt> callback returns +false+, all the later callbacks and the associated action are
+ # cancelled. If an <tt>after_*</tt> callback returns +false+, all the later callbacks are cancelled.
+ # Callbacks are generally run in the order they are defined, with the exception of callbacks defined as
# methods on the model, which are called last.
#
# == Transactions
@@ -218,16 +214,6 @@ module ActiveRecord
# needs to be aware of it because an ordinary +save+ will raise such exception
# instead of quietly returning +false+.
#
- # == Debugging callbacks
- #
- # To list the methods and procs registered with a particular callback, append <tt>_callback_chain</tt> to
- # the callback name that you wish to list and send that to your class from the Rails console:
- #
- # >> Topic.after_save_callback_chain
- # => [#<ActiveSupport::Callbacks::Callback:0x3f6a448
- # @method=#<Proc:0x03f9a42c@/Users/foo/bar/app/models/topic.rb:43>, kind:after_save, identifiernil,
- # options{}]
- #
module Callbacks
extend ActiveSupport::Concern
@@ -235,7 +221,7 @@ module ActiveRecord
:after_initialize, :after_find, :after_touch, :before_validation, :after_validation,
:before_save, :around_save, :after_save, :before_create, :around_create,
:after_create, :before_update, :around_update, :after_update,
- :before_destroy, :around_destroy, :after_destroy
+ :before_destroy, :around_destroy, :after_destroy, :after_commit, :after_rollback
]
included do
@@ -246,43 +232,26 @@ module ActiveRecord
define_model_callbacks :save, :create, :update, :destroy
end
- module ClassMethods
- def method_added(meth)
- super
- if CALLBACKS.include?(meth.to_sym)
- ActiveSupport::Deprecation.warn("Base##{meth} has been deprecated, please use Base.#{meth} :method instead", caller[0,1])
- send(meth.to_sym, meth.to_sym)
- end
- end
- end
-
def destroy #:nodoc:
- _run_destroy_callbacks { super }
+ run_callbacks(:destroy) { super }
end
def touch(*) #:nodoc:
- _run_touch_callbacks { super }
- end
-
- def deprecated_callback_method(symbol) #:nodoc:
- if respond_to?(symbol, true)
- ActiveSupport::Deprecation.warn("Overwriting #{symbol} in your models has been deprecated, please use Base##{symbol} :method_name instead")
- send(symbol)
- end
+ run_callbacks(:touch) { super }
end
private
def create_or_update #:nodoc:
- _run_save_callbacks { super }
+ run_callbacks(:save) { super }
end
def create #:nodoc:
- _run_create_callbacks { super }
+ run_callbacks(:create) { super }
end
def update(*) #:nodoc:
- _run_update_callbacks { super }
+ run_callbacks(:update) { super }
end
end
end
diff --git a/activerecord/lib/active_record/coders/yaml_column.rb b/activerecord/lib/active_record/coders/yaml_column.rb
new file mode 100644
index 0000000000..fb59d9fb07
--- /dev/null
+++ b/activerecord/lib/active_record/coders/yaml_column.rb
@@ -0,0 +1,41 @@
+module ActiveRecord
+ # :stopdoc:
+ module Coders
+ class YAMLColumn
+ RESCUE_ERRORS = [ ArgumentError ]
+
+ if defined?(Psych) && defined?(Psych::SyntaxError)
+ RESCUE_ERRORS << Psych::SyntaxError
+ end
+
+ attr_accessor :object_class
+
+ def initialize(object_class = Object)
+ @object_class = object_class
+ end
+
+ def dump(obj)
+ YAML.dump obj
+ end
+
+ def load(yaml)
+ return object_class.new if object_class != Object && yaml.nil?
+ return yaml unless yaml.is_a?(String) && yaml =~ /^---/
+ begin
+ obj = YAML.load(yaml)
+
+ unless obj.is_a?(object_class) || obj.nil?
+ raise SerializationTypeMismatch,
+ "Attribute was supposed to be a #{object_class}, but was a #{obj.class}"
+ end
+ obj ||= object_class.new if object_class != Object
+
+ obj
+ rescue *RESCUE_ERRORS
+ yaml
+ end
+ end
+ end
+ end
+ # :startdoc
+end
diff --git a/activerecord/lib/active_record/connection_adapters/abstract/connection_pool.rb b/activerecord/lib/active_record/connection_adapters/abstract/connection_pool.rb
index 02a8f4e214..4297c26413 100644
--- a/activerecord/lib/active_record/connection_adapters/abstract/connection_pool.rb
+++ b/activerecord/lib/active_record/connection_adapters/abstract/connection_pool.rb
@@ -1,3 +1,4 @@
+require 'thread'
require 'monitor'
require 'set'
require 'active_support/core_ext/module/synchronization'
@@ -56,7 +57,9 @@ module ActiveRecord
# * +wait_timeout+: number of seconds to block and wait for a connection
# before giving up and raising a timeout error (default 5 seconds).
class ConnectionPool
+ attr_accessor :automatic_reconnect
attr_reader :spec, :connections
+ attr_reader :columns, :columns_hash, :primary_keys, :tables
# Creates a new ConnectionPool object. +spec+ is a ConnectionSpecification
# object which describes database connection information (e.g. adapter,
@@ -73,18 +76,70 @@ module ActiveRecord
# The mutex used to synchronize pool access
@connection_mutex = Monitor.new
@queue = @connection_mutex.new_cond
-
- # default 5 second timeout unless on ruby 1.9
- @timeout =
- if RUBY_VERSION < '1.9'
- spec.config[:wait_timeout] || 5
- end
+ @timeout = spec.config[:wait_timeout] || 5
# default max pool size to 5
@size = (spec.config[:pool] && spec.config[:pool].to_i) || 5
@connections = []
@checked_out = []
+ @automatic_reconnect = true
+ @tables = {}
+
+ @columns = Hash.new do |h, table_name|
+ h[table_name] = with_connection do |conn|
+
+ # Fetch a list of columns
+ conn.columns(table_name, "#{table_name} Columns").tap do |columns|
+
+ # set primary key information
+ columns.each do |column|
+ column.primary = column.name == primary_keys[table_name]
+ end
+ end
+ end
+ end
+
+ @columns_hash = Hash.new do |h, table_name|
+ h[table_name] = Hash[columns[table_name].map { |col|
+ [col.name, col]
+ }]
+ end
+
+ @primary_keys = Hash.new do |h, table_name|
+ h[table_name] = with_connection do |conn|
+ table_exists?(table_name) ? conn.primary_key(table_name) : 'id'
+ end
+ end
+ end
+
+ # A cached lookup for table existence
+ def table_exists?(name)
+ return true if @tables.key? name
+
+ with_connection do |conn|
+ conn.tables.each { |table| @tables[table] = true }
+ end
+
+ @tables.key? name
+ end
+
+ # Clears out internal caches:
+ #
+ # * columns
+ # * columns_hash
+ # * tables
+ def clear_cache!
+ @columns.clear
+ @columns_hash.clear
+ @tables.clear
+ end
+
+ # Clear out internal caches for table with +table_name+
+ def clear_table_cache!(table_name)
+ @columns.delete table_name
+ @columns_hash.delete table_name
+ @primary_keys.delete table_name
end
# Retrieve the connection associated with the current thread, or call
@@ -93,11 +148,7 @@ module ActiveRecord
# #connection can be called any number of times; the connection is
# held in a hash keyed by the thread id.
def connection
- if conn = @reserved_connections[current_connection_id]
- conn
- else
- @reserved_connections[current_connection_id] = checkout
- end
+ @reserved_connections[current_connection_id] ||= checkout
end
# Signal that the thread is finished with the current connection.
@@ -109,7 +160,7 @@ module ActiveRecord
end
# If a connection already exists yield it to the block. If no connection
- # exists checkout a connection, yield it to the block, and checkin the
+ # exists checkout a connection, yield it to the block, and checkin the
# connection when finished.
def with_connection
connection_id = current_connection_id
@@ -165,7 +216,6 @@ module ActiveRecord
keys = @reserved_connections.keys - Thread.list.find_all { |t|
t.alive?
}.map { |thread| thread.object_id }
-
keys.each do |key|
checkin @reserved_connections[key]
@reserved_connections.delete(key)
@@ -198,16 +248,18 @@ module ActiveRecord
checkout_new_connection
end
return conn if conn
- # No connections available; wait for one
- if @queue.wait(@timeout)
+
+ @queue.wait(@timeout)
+
+ if(@checked_out.size < @connections.size)
next
else
- # try looting dead threads
clear_stale_cached_connections!
if @size == @checked_out.size
raise ConnectionTimeoutError, "could not obtain a database connection#{" within #{@timeout} seconds" if @timeout}. The max pool size is currently #{@size}; consider increasing it."
end
end
+
end
end
end
@@ -219,7 +271,7 @@ module ActiveRecord
# calling +checkout+ on this pool.
def checkin(conn)
@connection_mutex.synchronize do
- conn.send(:_run_checkin_callbacks) do
+ conn.run_callbacks :checkin do
@checked_out.delete conn
@queue.signal
end
@@ -239,6 +291,8 @@ module ActiveRecord
end
def checkout_new_connection
+ raise ConnectionNotEstablished unless @automatic_reconnect
+
c = new_connection
@connections << c
checkout_and_verify(c)
@@ -326,7 +380,7 @@ module ActiveRecord
# already been opened.
def connected?(klass)
conn = retrieve_connection_pool(klass)
- conn ? conn.connected? : false
+ conn && conn.connected?
end
# Remove the connection for this class. This will close the active
@@ -337,7 +391,7 @@ module ActiveRecord
pool = @connection_pools[klass.name]
return nil unless pool
- @connection_pools.delete_if { |key, value| value == pool }
+ pool.automatic_reconnect = false
pool.disconnect!
pool.spec.config
end
diff --git a/activerecord/lib/active_record/connection_adapters/abstract/connection_specification.rb b/activerecord/lib/active_record/connection_adapters/abstract/connection_specification.rb
index 8e74eff0ab..d88720c8bf 100644
--- a/activerecord/lib/active_record/connection_adapters/abstract/connection_specification.rb
+++ b/activerecord/lib/active_record/connection_adapters/abstract/connection_specification.rb
@@ -67,12 +67,12 @@ module ActiveRecord
begin
require "active_record/connection_adapters/#{spec[:adapter]}_adapter"
- rescue LoadError
- raise "Please install the #{spec[:adapter]} adapter: `gem install activerecord-#{spec[:adapter]}-adapter` (#{$!})"
+ rescue LoadError => e
+ raise "Please install the #{spec[:adapter]} adapter: `gem install activerecord-#{spec[:adapter]}-adapter` (#{e})"
end
adapter_method = "#{spec[:adapter]}_connection"
- if !respond_to?(adapter_method)
+ unless respond_to?(adapter_method)
raise AdapterNotFound, "database configuration specifies nonexistent #{spec[:adapter]} adapter"
end
@@ -89,6 +89,16 @@ module ActiveRecord
retrieve_connection
end
+ # Returns the configuration of the associated connection as a hash:
+ #
+ # ActiveRecord::Base.connection_config
+ # # => {:pool=>5, :timeout=>5000, :database=>"db/development.sqlite3", :adapter=>"sqlite3"}
+ #
+ # Please use only for reading.
+ def connection_config
+ connection_pool.spec.config
+ end
+
def connection_pool
connection_handler.retrieve_connection_pool(self)
end
diff --git a/activerecord/lib/active_record/connection_adapters/abstract/database_limits.rb b/activerecord/lib/active_record/connection_adapters/abstract/database_limits.rb
index a130c330dd..29ac9341ec 100644
--- a/activerecord/lib/active_record/connection_adapters/abstract/database_limits.rb
+++ b/activerecord/lib/active_record/connection_adapters/abstract/database_limits.rb
@@ -37,9 +37,9 @@ module ActiveRecord
16
end
- # the maximum number of elements in an IN (x,y,z) clause
+ # the maximum number of elements in an IN (x,y,z) clause. nil means no limit
def in_clause_length
- 65535
+ nil
end
# the maximum length of an SQL query
diff --git a/activerecord/lib/active_record/connection_adapters/abstract/database_statements.rb b/activerecord/lib/active_record/connection_adapters/abstract/database_statements.rb
index 25432e9985..a3082b8f01 100644
--- a/activerecord/lib/active_record/connection_adapters/abstract/database_statements.rb
+++ b/activerecord/lib/active_record/connection_adapters/abstract/database_statements.rb
@@ -1,10 +1,20 @@
+require 'active_support/core_ext/module/deprecation'
+
module ActiveRecord
module ConnectionAdapters # :nodoc:
module DatabaseStatements
# Returns an array of record hashes with the column names as keys and
# column values as values.
- def select_all(sql, name = nil)
- select(sql, name)
+ def select_all(sql, name = nil, binds = [])
+ if supports_statement_cache?
+ select(sql, name, binds)
+ else
+ return select(sql, name) if binds.empty?
+ binds = binds.dup
+ select sql.gsub('?') {
+ quote(*binds.shift.reverse)
+ }, name
+ end
end
# Returns a record hash with the column names as keys and column values
@@ -35,10 +45,16 @@ module ActiveRecord
undef_method :select_rows
# Executes the SQL statement in the context of this connection.
- def execute(sql, name = nil, skip_logging = false)
+ def execute(sql, name = nil)
end
undef_method :execute
+ # Executes +sql+ statement in the context of this connection using
+ # +binds+ as the bind substitutes. +name+ is logged along with
+ # the executed +sql+ statement.
+ def exec_query(sql, name = 'SQL', binds = [])
+ end
+
# Returns the last auto-generated ID from the affected table.
def insert(sql, name = nil, pk = nil, id_value = nil, sequence_name = nil)
insert_sql(sql, name, pk, id_value, sequence_name)
@@ -68,6 +84,12 @@ module ActiveRecord
nil
end
+ # Returns +true+ when the connection adapter supports prepared statement
+ # caching, otherwise returns +false+
+ def supports_statement_cache?
+ false
+ end
+
# Runs the given block in a database transaction, and returns the result
# of the block.
#
@@ -209,11 +231,12 @@ module ActiveRecord
#
# This method *modifies* the +sql+ parameter.
#
+ # This method is deprecated!! Stop using it!
+ #
# ===== Examples
# add_limit_offset!('SELECT * FROM suppliers', {:limit => 10, :offset => 50})
# generates
# SELECT * FROM suppliers LIMIT 10 OFFSET 50
-
def add_limit_offset!(sql, options)
if limit = options[:limit]
sql << " LIMIT #{sanitize_limit(limit)}"
@@ -223,6 +246,7 @@ module ActiveRecord
end
sql
end
+ deprecate :add_limit_offset!
def default_sequence_name(table, column)
nil
@@ -236,7 +260,19 @@ module ActiveRecord
# Inserts the given fixture into the table. Overridden in adapters that require
# something beyond a simple insert (eg. Oracle).
def insert_fixture(fixture, table_name)
- execute "INSERT INTO #{quote_table_name(table_name)} (#{fixture.key_list}) VALUES (#{fixture.value_list})", 'Fixture Insert'
+ columns = Hash[columns(table_name).map { |c| [c.name, c] }]
+
+ key_list = []
+ value_list = fixture.map do |name, value|
+ key_list << quote_column_name(name)
+ quote(value, columns[name])
+ end
+
+ execute "INSERT INTO #{quote_table_name(table_name)} (#{key_list.join(', ')}) VALUES (#{value_list.join(', ')})", 'Fixture Insert'
+ end
+
+ def null_insert_value
+ Arel.sql 'DEFAULT'
end
def empty_insert_statement_value
@@ -251,10 +287,29 @@ module ActiveRecord
"WHERE #{quoted_primary_key} IN (SELECT #{quoted_primary_key} FROM #{quoted_table_name} #{where_sql})"
end
+ # Sanitizes the given LIMIT parameter in order to prevent SQL injection.
+ #
+ # The +limit+ may be anything that can evaluate to a string via #to_s. It
+ # should look like an integer, or a comma-delimited list of integers, or
+ # an Arel SQL literal.
+ #
+ # Returns Integer and Arel::Nodes::SqlLiteral limits as is.
+ # Returns the sanitized limit parameter, either as an integer, or as a
+ # string which contains a comma-delimited list of integers.
+ def sanitize_limit(limit)
+ if limit.is_a?(Integer) || limit.is_a?(Arel::Nodes::SqlLiteral)
+ limit
+ elsif limit.to_s =~ /,/
+ Arel.sql limit.to_s.split(',').map{ |i| Integer(i) }.join(',')
+ else
+ Integer(limit)
+ end
+ end
+
protected
# Returns an array of record hashes with the column names as keys and
# column values as values.
- def select(sql, name = nil)
+ def select(sql, name = nil, binds = [])
end
undef_method :select
@@ -274,21 +329,6 @@ module ActiveRecord
update_sql(sql, name)
end
- # Sanitizes the given LIMIT parameter in order to prevent SQL injection.
- #
- # +limit+ may be anything that can evaluate to a string via #to_s. It
- # should look like an integer, or a comma-delimited list of integers.
- #
- # Returns the sanitized limit parameter, either as an integer, or as a
- # string which contains a comma-delimited list of integers.
- def sanitize_limit(limit)
- if limit.to_s =~ /,/
- limit.to_s.split(',').map{ |i| i.to_i }.join(',')
- else
- limit.to_i
- end
- end
-
# Send a rollback message to all records after they have been rolled back. If rollback
# is false, only rollback records since the last save point.
def rollback_transaction_records(rollback) #:nodoc
diff --git a/activerecord/lib/active_record/connection_adapters/abstract/query_cache.rb b/activerecord/lib/active_record/connection_adapters/abstract/query_cache.rb
index 78fffaff6e..1db397f584 100644
--- a/activerecord/lib/active_record/connection_adapters/abstract/query_cache.rb
+++ b/activerecord/lib/active_record/connection_adapters/abstract/query_cache.rb
@@ -1,5 +1,3 @@
-require 'active_support/core_ext/object/duplicable'
-
module ActiveRecord
module ConnectionAdapters # :nodoc:
module QueryCache
@@ -49,40 +47,26 @@ module ActiveRecord
@query_cache.clear
end
- def select_all(*args)
- if @query_cache_enabled
- cache_sql(args.first) { super }
- else
- super
- end
- end
-
- def columns(*)
+ def select_all(sql, name = nil, binds = [])
if @query_cache_enabled
- @query_cache["SHOW FIELDS FROM #{args.first}"] ||= super
+ cache_sql(sql, binds) { super }
else
super
end
end
private
- def cache_sql(sql)
+ def cache_sql(sql, binds)
result =
- if @query_cache.has_key?(sql)
+ if @query_cache[sql].key?(binds)
ActiveSupport::Notifications.instrument("sql.active_record",
- :sql => sql, :name => "CACHE", :connection_id => self.object_id)
- @query_cache[sql]
+ :sql => sql, :name => "CACHE", :connection_id => object_id)
+ @query_cache[sql][binds]
else
- @query_cache[sql] = yield
+ @query_cache[sql][binds] = yield
end
- if Array === result
- result.collect { |row| row.dup }
- else
- result.duplicable? ? result.dup : result
- end
- rescue TypeError
- result
+ result.collect { |row| row.dup }
end
end
end
diff --git a/activerecord/lib/active_record/connection_adapters/abstract/quoting.rb b/activerecord/lib/active_record/connection_adapters/abstract/quoting.rb
index e2b3773a99..7489e88eef 100644
--- a/activerecord/lib/active_record/connection_adapters/abstract/quoting.rb
+++ b/activerecord/lib/active_record/connection_adapters/abstract/quoting.rb
@@ -10,28 +10,32 @@ module ActiveRecord
return value.quoted_id if value.respond_to?(:quoted_id)
case value
- when String, ActiveSupport::Multibyte::Chars
- value = value.to_s
- if column && column.type == :binary && column.class.respond_to?(:string_to_binary)
- "'#{quote_string(column.class.string_to_binary(value))}'" # ' (for ruby-mode)
- elsif column && [:integer, :float].include?(column.type)
- value = column.type == :integer ? value.to_i : value.to_f
- value.to_s
- else
- "'#{quote_string(value)}'" # ' (for ruby-mode)
- end
- when NilClass then "NULL"
- when TrueClass then (column && column.type == :integer ? '1' : quoted_true)
- when FalseClass then (column && column.type == :integer ? '0' : quoted_false)
- when Float, Fixnum, Bignum then value.to_s
- # BigDecimals need to be output in a non-normalized form and quoted.
- when BigDecimal then value.to_s('F')
+ when String, ActiveSupport::Multibyte::Chars
+ value = value.to_s
+ return "'#{quote_string(value)}'" unless column
+
+ case column.type
+ when :binary then "'#{quote_string(column.string_to_binary(value))}'"
+ when :integer then value.to_i.to_s
+ when :float then value.to_f.to_s
+ else
+ "'#{quote_string(value)}'"
+ end
+
+ when true, false
+ if column && column.type == :integer
+ value ? '1' : '0'
else
- if value.acts_like?(:date) || value.acts_like?(:time)
- "'#{quoted_date(value)}'"
- else
- "'#{quote_string(value.to_s)}'"
- end
+ value ? quoted_true : quoted_false
+ end
+ # BigDecimals need to be put in a non-normalized form and quoted.
+ when nil then "NULL"
+ when BigDecimal then value.to_s('F')
+ when Numeric then value.to_s
+ when Date, Time then "'#{quoted_date(value)}'"
+ when Symbol then "'#{quote_string(value.to_s)}'"
+ else
+ "'#{quote_string(value.to_yaml)}'"
end
end
diff --git a/activerecord/lib/active_record/connection_adapters/abstract/schema_definitions.rb b/activerecord/lib/active_record/connection_adapters/abstract/schema_definitions.rb
index 9118ceb33c..7ac48c6646 100644
--- a/activerecord/lib/active_record/connection_adapters/abstract/schema_definitions.rb
+++ b/activerecord/lib/active_record/connection_adapters/abstract/schema_definitions.rb
@@ -6,259 +6,6 @@ require 'bigdecimal/util'
module ActiveRecord
module ConnectionAdapters #:nodoc:
- # An abstract definition of a column in a table.
- class Column
- TRUE_VALUES = [true, 1, '1', 't', 'T', 'true', 'TRUE'].to_set
- FALSE_VALUES = [false, 0, '0', 'f', 'F', 'false', 'FALSE'].to_set
-
- module Format
- ISO_DATE = /\A(\d{4})-(\d\d)-(\d\d)\z/
- ISO_DATETIME = /\A(\d{4})-(\d\d)-(\d\d) (\d\d):(\d\d):(\d\d)(\.\d+)?\z/
- end
-
- attr_reader :name, :default, :type, :limit, :null, :sql_type, :precision, :scale
- attr_accessor :primary
-
- # Instantiates a new column in the table.
- #
- # +name+ is the column's name, such as <tt>supplier_id</tt> in <tt>supplier_id int(11)</tt>.
- # +default+ is the type-casted default value, such as +new+ in <tt>sales_stage varchar(20) default 'new'</tt>.
- # +sql_type+ is used to extract the column's length, if necessary. For example +60+ in
- # <tt>company_name varchar(60)</tt>.
- # It will be mapped to one of the standard Rails SQL types in the <tt>type</tt> attribute.
- # +null+ determines if this column allows +NULL+ values.
- def initialize(name, default, sql_type = nil, null = true)
- @name, @sql_type, @null = name, sql_type, null
- @limit, @precision, @scale = extract_limit(sql_type), extract_precision(sql_type), extract_scale(sql_type)
- @type = simplified_type(sql_type)
- @default = extract_default(default)
-
- @primary = nil
- end
-
- # Returns +true+ if the column is either of type string or text.
- def text?
- type == :string || type == :text
- end
-
- # Returns +true+ if the column is either of type integer, float or decimal.
- def number?
- type == :integer || type == :float || type == :decimal
- end
-
- def has_default?
- !default.nil?
- end
-
- # Returns the Ruby class that corresponds to the abstract data type.
- def klass
- case type
- when :integer then Fixnum
- when :float then Float
- when :decimal then BigDecimal
- when :datetime then Time
- when :date then Date
- when :timestamp then Time
- when :time then Time
- when :text, :string then String
- when :binary then String
- when :boolean then Object
- end
- end
-
- # Casts value (which is a String) to an appropriate instance.
- def type_cast(value)
- return nil if value.nil?
- case type
- when :string then value
- when :text then value
- when :integer then value.to_i rescue value ? 1 : 0
- when :float then value.to_f
- when :decimal then self.class.value_to_decimal(value)
- when :datetime then self.class.string_to_time(value)
- when :timestamp then self.class.string_to_time(value)
- when :time then self.class.string_to_dummy_time(value)
- when :date then self.class.string_to_date(value)
- when :binary then self.class.binary_to_string(value)
- when :boolean then self.class.value_to_boolean(value)
- else value
- end
- end
-
- def type_cast_code(var_name)
- case type
- when :string then nil
- when :text then nil
- when :integer then "(#{var_name}.to_i rescue #{var_name} ? 1 : 0)"
- when :float then "#{var_name}.to_f"
- when :decimal then "#{self.class.name}.value_to_decimal(#{var_name})"
- when :datetime then "#{self.class.name}.string_to_time(#{var_name})"
- when :timestamp then "#{self.class.name}.string_to_time(#{var_name})"
- when :time then "#{self.class.name}.string_to_dummy_time(#{var_name})"
- when :date then "#{self.class.name}.string_to_date(#{var_name})"
- when :binary then "#{self.class.name}.binary_to_string(#{var_name})"
- when :boolean then "#{self.class.name}.value_to_boolean(#{var_name})"
- else nil
- end
- end
-
- # Returns the human name of the column name.
- #
- # ===== Examples
- # Column.new('sales_stage', ...).human_name # => 'Sales stage'
- def human_name
- Base.human_attribute_name(@name)
- end
-
- def extract_default(default)
- type_cast(default)
- end
-
- class << self
- # Used to convert from Strings to BLOBs
- def string_to_binary(value)
- value
- end
-
- # Used to convert from BLOBs to Strings
- def binary_to_string(value)
- value
- end
-
- def string_to_date(string)
- return string unless string.is_a?(String)
- return nil if string.empty?
-
- fast_string_to_date(string) || fallback_string_to_date(string)
- end
-
- def string_to_time(string)
- return string unless string.is_a?(String)
- return nil if string.empty?
-
- fast_string_to_time(string) || fallback_string_to_time(string)
- end
-
- def string_to_dummy_time(string)
- return string unless string.is_a?(String)
- return nil if string.empty?
-
- string_to_time "2000-01-01 #{string}"
- end
-
- # convert something to a boolean
- def value_to_boolean(value)
- if value.is_a?(String) && value.blank?
- nil
- else
- TRUE_VALUES.include?(value)
- end
- end
-
- # convert something to a BigDecimal
- def value_to_decimal(value)
- # Using .class is faster than .is_a? and
- # subclasses of BigDecimal will be handled
- # in the else clause
- if value.class == BigDecimal
- value
- elsif value.respond_to?(:to_d)
- value.to_d
- else
- value.to_s.to_d
- end
- end
-
- protected
- # '0.123456' -> 123456
- # '1.123456' -> 123456
- def microseconds(time)
- ((time[:sec_fraction].to_f % 1) * 1_000_000).to_i
- end
-
- def new_date(year, mon, mday)
- if year && year != 0
- Date.new(year, mon, mday) rescue nil
- end
- end
-
- def new_time(year, mon, mday, hour, min, sec, microsec)
- # Treat 0000-00-00 00:00:00 as nil.
- return nil if year.nil? || year == 0
-
- Time.time_with_datetime_fallback(Base.default_timezone, year, mon, mday, hour, min, sec, microsec) rescue nil
- end
-
- def fast_string_to_date(string)
- if string =~ Format::ISO_DATE
- new_date $1.to_i, $2.to_i, $3.to_i
- end
- end
-
- # Doesn't handle time zones.
- def fast_string_to_time(string)
- if string =~ Format::ISO_DATETIME
- microsec = ($7.to_f * 1_000_000).to_i
- new_time $1.to_i, $2.to_i, $3.to_i, $4.to_i, $5.to_i, $6.to_i, microsec
- end
- end
-
- def fallback_string_to_date(string)
- new_date(*::Date._parse(string, false).values_at(:year, :mon, :mday))
- end
-
- def fallback_string_to_time(string)
- time_hash = Date._parse(string)
- time_hash[:sec_fraction] = microseconds(time_hash)
-
- new_time(*time_hash.values_at(:year, :mon, :mday, :hour, :min, :sec, :sec_fraction))
- end
- end
-
- private
- def extract_limit(sql_type)
- $1.to_i if sql_type =~ /\((.*)\)/
- end
-
- def extract_precision(sql_type)
- $2.to_i if sql_type =~ /^(numeric|decimal|number)\((\d+)(,\d+)?\)/i
- end
-
- def extract_scale(sql_type)
- case sql_type
- when /^(numeric|decimal|number)\((\d+)\)/i then 0
- when /^(numeric|decimal|number)\((\d+)(,(\d+))\)/i then $4.to_i
- end
- end
-
- def simplified_type(field_type)
- case field_type
- when /int/i
- :integer
- when /float|double/i
- :float
- when /decimal|numeric|number/i
- extract_scale(field_type) == 0 ? :integer : :decimal
- when /datetime/i
- :datetime
- when /timestamp/i
- :timestamp
- when /time/i
- :time
- when /date/i
- :date
- when /clob/i, /text/i
- :text
- when /blob/i, /binary/i
- :binary
- when /char/i, /string/i
- :string
- when /boolean/i
- :boolean
- end
- end
- end
-
class IndexDefinition < Struct.new(:table, :name, :unique, :columns, :lengths) #:nodoc:
end
@@ -268,6 +15,10 @@ module ActiveRecord
# for generating a number of table creation or table changing SQL statements.
class ColumnDefinition < Struct.new(:base, :name, :type, :limit, :precision, :scale, :default, :null) #:nodoc:
+ def string_to_binary(value)
+ value
+ end
+
def sql_type
base.type_to_sql(type.to_sym, limit, precision, scale) rescue type
end
@@ -318,21 +69,13 @@ module ActiveRecord
@base = base
end
- #Handles non supported datatypes - e.g. XML
- def method_missing(symbol, *args)
- if symbol.to_s == 'xml'
- xml_column_fallback(args)
- else
- super
- end
- end
+ def xml(*args)
+ raise NotImplementedError unless %w{
+ sqlite mysql mysql2
+ }.include? @base.adapter_name.downcase
- def xml_column_fallback(*args)
- case @base.adapter_name.downcase
- when 'sqlite', 'mysql'
- options = args.extract_options!
- column(args[0], :text, options)
- end
+ options = args.extract_options!
+ column(args[0], :text, options)
end
# Appends a primary key definition to the table definition.
@@ -360,7 +103,7 @@ module ActiveRecord
#
# Available options are (none of these exists by default):
# * <tt>:limit</tt> -
- # Requests a maximum column length. This is number of characters for <tt>:string</tt> and
+ # Requests a maximum column length. This is number of characters for <tt>:string</tt> and
# <tt>:text</tt> columns and number of bytes for :binary and :integer columns.
# * <tt>:default</tt> -
# The column's default value. Use nil for NULL.
@@ -464,7 +207,7 @@ module ActiveRecord
# TableDefinition#timestamps that'll add created_at and +updated_at+ as datetimes.
#
# TableDefinition#references will add an appropriately-named _id column, plus a corresponding _type
- # column if the <tt>:polymorphic</tt> option is supplied. If <tt>:polymorphic</tt> is a hash of
+ # column if the <tt>:polymorphic</tt> option is supplied. If <tt>:polymorphic</tt> is a hash of
# options, these will be used when creating the <tt>_type</tt> column. So what can be written like this:
#
# create_table :taggings do |t|
@@ -528,7 +271,7 @@ module ActiveRecord
# concatenated together. This string can then be prepended and appended to
# to generate the final SQL to create the table.
def to_sql
- @columns.map(&:to_sql) * ', '
+ @columns.map { |c| c.to_sql } * ', '
end
private
diff --git a/activerecord/lib/active_record/connection_adapters/abstract/schema_statements.rb b/activerecord/lib/active_record/connection_adapters/abstract/schema_statements.rb
index 7dee68502f..8bae50885f 100644
--- a/activerecord/lib/active_record/connection_adapters/abstract/schema_statements.rb
+++ b/activerecord/lib/active_record/connection_adapters/abstract/schema_statements.rb
@@ -12,7 +12,7 @@ module ActiveRecord
# Truncates a table alias according to the limits of the current adapter.
def table_alias_for(table_name)
- table_name[0..table_alias_length-1].gsub(/\./, '_')
+ table_name[0...table_alias_length].gsub(/\./, '_')
end
# def tables(name = nil) end
@@ -110,8 +110,8 @@ module ActiveRecord
#
# Also note that this just sets the primary key in the table. You additionally
# need to configure the primary key in the model via the +set_primary_key+ macro.
- # Models do NOT auto-detect the primary key from their table definition.
- #
+ # Models do NOT auto-detect the primary key from their table definition.
+ #
# [<tt>:options</tt>]
# Any extra options you want appended to the table definition.
# [<tt>:temporary</tt>]
@@ -151,10 +151,10 @@ module ActiveRecord
#
# See also TableDefinition#column for details on how to create columns.
def create_table(table_name, options = {})
- table_definition = TableDefinition.new(self)
- table_definition.primary_key(options[:primary_key] || Base.get_primary_key(table_name.to_s.singularize)) unless options[:id] == false
+ td = table_definition
+ td.primary_key(options[:primary_key] || Base.get_primary_key(table_name.to_s.singularize)) unless options[:id] == false
- yield table_definition if block_given?
+ yield td if block_given?
if options[:force] && table_exists?(table_name)
drop_table(table_name, options)
@@ -162,7 +162,7 @@ module ActiveRecord
create_sql = "CREATE#{' TEMPORARY' if options[:temporary]} TABLE "
create_sql << "#{quote_table_name(table_name)} ("
- create_sql << table_definition.to_sql
+ create_sql << td.to_sql
create_sql << ") #{options[:options]}"
execute create_sql
end
@@ -176,6 +176,13 @@ module ActiveRecord
# # Other column alterations here
# end
#
+ # The +options+ hash can include the following keys:
+ # [<tt>:bulk</tt>]
+ # Set this to true to make this a bulk alter query, such as
+ # ALTER TABLE `users` ADD COLUMN age INT(11), ADD COLUMN birthdate DATETIME ...
+ #
+ # Defaults to false.
+ #
# ===== Examples
# ====== Add a column
# change_table(:suppliers) do |t|
@@ -224,8 +231,14 @@ module ActiveRecord
#
# See also Table for details on
# all of the various column transformation
- def change_table(table_name)
- yield Table.new(table_name, self)
+ def change_table(table_name, options = {})
+ if supports_bulk_alter? && options[:bulk]
+ recorder = ActiveRecord::Migration::CommandRecorder.new(self)
+ yield Table.new(table_name, recorder)
+ bulk_change_table(table_name, recorder.commands)
+ else
+ yield Table.new(table_name, self)
+ end
end
# Renames a table.
@@ -253,10 +266,7 @@ module ActiveRecord
# remove_column(:suppliers, :qualification)
# remove_columns(:suppliers, :qualification, :experience)
def remove_column(table_name, *column_names)
- raise ArgumentError.new("You must specify at least one column name. Example: remove_column(:people, :first_name)") if column_names.empty?
- column_names.flatten.each do |column_name|
- execute "ALTER TABLE #{quote_table_name(table_name)} DROP #{quote_column_name(column_name)}"
- end
+ columns_for_remove(table_name, *column_names).each {|column_name| execute "ALTER TABLE #{quote_table_name(table_name)} DROP #{column_name}" }
end
alias :remove_columns :remove_column
@@ -269,12 +279,11 @@ module ActiveRecord
raise NotImplementedError, "change_column is not implemented"
end
- # Sets a new default value for a column. If you want to set the default
- # value to +NULL+, you are out of luck. You need to
- # DatabaseStatements#execute the appropriate SQL statement yourself.
+ # Sets a new default value for a column.
# ===== Examples
# change_column_default(:suppliers, :qualification, 'new')
# change_column_default(:accounts, :authorized, 1)
+ # change_column_default(:users, :email, nil)
def change_column_default(table_name, column_name, default)
raise NotImplementedError, "change_column_default is not implemented"
end
@@ -327,29 +336,8 @@ module ActiveRecord
#
# Note: SQLite doesn't support index length
def add_index(table_name, column_name, options = {})
- options[:name] = options[:name].to_s if options.key?(:name)
-
- column_names = Array.wrap(column_name)
- index_name = index_name(table_name, :column => column_names)
-
- if Hash === options # legacy support, since this param was a string
- index_type = options[:unique] ? "UNIQUE" : ""
- index_name = options[:name] || index_name
- else
- index_type = options
- end
-
- if index_name.length > index_name_length
- @logger.warn("Index name '#{index_name}' on table '#{table_name}' is too long; the limit is #{index_name_length} characters. Skipping.")
- return
- end
- if index_name_exists?(table_name, index_name, false)
- @logger.warn("Index name '#{index_name}' on table '#{table_name}' already exists. Skipping.")
- return
- end
- quoted_column_names = quoted_columns_for_index(column_names, options).join(", ")
-
- execute "CREATE #{index_type} INDEX #{quote_column_name(index_name)} ON #{quote_table_name(table_name)} (#{quoted_column_names})"
+ index_name, index_type, index_columns = add_index_options(table_name, column_name, options)
+ execute "CREATE #{index_type} INDEX #{quote_column_name(index_name)} ON #{quote_table_name(table_name)} (#{index_columns})"
end
# Remove the given index from the table.
@@ -363,12 +351,7 @@ module ActiveRecord
# Remove the index named by_branch_party in the accounts table.
# remove_index :accounts, :name => :by_branch_party
def remove_index(table_name, options = {})
- index_name = index_name(table_name, options)
- unless index_name_exists?(table_name, index_name, true)
- @logger.warn("Index name '#{index_name}' on table '#{table_name}' does not exist. Skipping.")
- return
- end
- remove_index!(table_name, index_name)
+ remove_index!(table_name, index_name_for_remove(table_name, options))
end
def remove_index!(table_name, index_name) #:nodoc:
@@ -407,6 +390,7 @@ module ActiveRecord
# as there's no way to determine the correct answer in that case.
def index_name_exists?(table_name, index_name, default)
return default unless respond_to?(:indexes)
+ index_name = index_name.to_s
indexes(table_name).detect { |i| i.name == index_name }
end
@@ -446,12 +430,14 @@ module ActiveRecord
end
end
- def assume_migrated_upto_version(version, migrations_path = ActiveRecord::Migrator.migrations_path)
+ def assume_migrated_upto_version(version, migrations_paths = ActiveRecord::Migrator.migrations_paths)
+ migrations_paths = Array.wrap(migrations_paths)
version = version.to_i
sm_table = quote_table_name(ActiveRecord::Migrator.schema_migrations_table_name)
- migrated = select_values("SELECT version FROM #{sm_table}").map(&:to_i)
- versions = Dir["#{migrations_path}/[0-9]*_*.rb"].map do |filename|
+ migrated = select_values("SELECT version FROM #{sm_table}").map { |v| v.to_i }
+ paths = migrations_paths.map {|p| "#{p}/[0-9]*_*.rb" }
+ versions = Dir[*paths].map do |filename|
filename.split('/').last.split('_').first.to_i
end
@@ -471,7 +457,7 @@ module ActiveRecord
end
def type_to_sql(type, limit = nil, precision = nil, scale = nil) #:nodoc:
- if native = native_database_types[type]
+ if native = native_database_types[type.to_sym]
column_type_sql = (native.is_a?(Hash) ? native[:name] : native).dup
if type == :decimal # ignore limit, use precision and scale
@@ -538,6 +524,50 @@ module ActiveRecord
def options_include_default?(options)
options.include?(:default) && !(options[:null] == false && options[:default].nil?)
end
+
+ def add_index_options(table_name, column_name, options = {})
+ column_names = Array.wrap(column_name)
+ index_name = index_name(table_name, :column => column_names)
+
+ if Hash === options # legacy support, since this param was a string
+ index_type = options[:unique] ? "UNIQUE" : ""
+ index_name = options[:name].to_s if options.key?(:name)
+ else
+ index_type = options
+ end
+
+ if index_name.length > index_name_length
+ raise ArgumentError, "Index name '#{index_name}' on table '#{table_name}' is too long; the limit is #{index_name_length} characters"
+ end
+ if index_name_exists?(table_name, index_name, false)
+ raise ArgumentError, "Index name '#{index_name}' on table '#{table_name}' already exists"
+ end
+ index_columns = quoted_columns_for_index(column_names, options).join(", ")
+
+ [index_name, index_type, index_columns]
+ end
+
+ def index_name_for_remove(table_name, options = {})
+ index_name = index_name(table_name, options)
+
+ unless index_name_exists?(table_name, index_name, true)
+ raise ArgumentError, "Index name '#{index_name}' on table '#{table_name}' does not exist"
+ end
+
+ index_name
+ end
+
+ def columns_for_remove(table_name, *column_names)
+ column_names = column_names.flatten
+
+ raise ArgumentError.new("You must specify at least one column name. Example: remove_column(:people, :first_name)") if column_names.blank?
+ column_names.map {|column_name| quote_column_name(column_name) }
+ end
+
+ private
+ def table_definition
+ TableDefinition.new(self)
+ end
end
end
end
diff --git a/activerecord/lib/active_record/connection_adapters/abstract_adapter.rb b/activerecord/lib/active_record/connection_adapters/abstract_adapter.rb
index d8c92d0ad3..0f44baa2fe 100644
--- a/activerecord/lib/active_record/connection_adapters/abstract_adapter.rb
+++ b/activerecord/lib/active_record/connection_adapters/abstract_adapter.rb
@@ -4,6 +4,7 @@ require 'bigdecimal/util'
require 'active_support/core_ext/benchmark'
# TODO: Autoload these files
+require 'active_record/connection_adapters/column'
require 'active_record/connection_adapters/abstract/schema_definitions'
require 'active_record/connection_adapters/abstract/schema_statements'
require 'active_record/connection_adapters/abstract/database_statements'
@@ -12,6 +13,7 @@ require 'active_record/connection_adapters/abstract/connection_pool'
require 'active_record/connection_adapters/abstract/connection_specification'
require 'active_record/connection_adapters/abstract/query_cache'
require 'active_record/connection_adapters/abstract/database_limits'
+require 'active_record/result'
module ActiveRecord
module ConnectionAdapters # :nodoc:
@@ -40,7 +42,7 @@ module ActiveRecord
@active = nil
@connection, @logger = connection, logger
@query_cache_enabled = false
- @query_cache = {}
+ @query_cache = Hash.new { |h,sql| h[sql] = {} }
@instrumenter = ActiveSupport::Notifications.instrumenter
end
@@ -76,8 +78,12 @@ module ActiveRecord
false
end
- # Does this adapter support savepoints? PostgreSQL and MySQL do, SQLite
- # does not.
+ def supports_bulk_alter?
+ false
+ end
+
+ # Does this adapter support savepoints? PostgreSQL and MySQL do,
+ # SQLite < 3.6.8 does not.
def supports_savepoints?
false
end
@@ -97,6 +103,12 @@ module ActiveRecord
quote_column_name(name)
end
+ # Returns a bind substitution value given a +column+ and list of current
+ # +binds+
+ def substitute_for(column, binds)
+ Arel.sql '?'
+ end
+
# REFERENTIAL INTEGRITY ====================================
# Override to turn off referential integrity while executing <tt>&block</tt>.
@@ -135,6 +147,13 @@ module ActiveRecord
# this should be overridden by concrete adapters
end
+ ###
+ # Clear any caching the database adapter may be doing, for example
+ # clearing the prepared statement cache. This is database specific.
+ def clear_cache!
+ # this should be overridden by concrete adapters
+ end
+
# Returns true if its required to reload the connection between requests for development mode.
# This is not the case for Ruby/MySQL and it's not necessary for any adapters except SQLite.
def requires_reloading?
@@ -190,12 +209,13 @@ module ActiveRecord
protected
- def log(sql, name)
- name ||= "SQL"
- @instrumenter.instrument("sql.active_record",
- :sql => sql, :name => name, :connection_id => object_id) do
- yield
- end
+ def log(sql, name = "SQL", binds = [])
+ @instrumenter.instrument(
+ "sql.active_record",
+ :sql => sql,
+ :name => name,
+ :connection_id => object_id,
+ :binds => binds) { yield }
rescue Exception => e
message = "#{e.class.name}: #{e.message}: #{sql}"
@logger.debug message if @logger
diff --git a/activerecord/lib/active_record/connection_adapters/column.rb b/activerecord/lib/active_record/connection_adapters/column.rb
new file mode 100644
index 0000000000..4e3d8a096f
--- /dev/null
+++ b/activerecord/lib/active_record/connection_adapters/column.rb
@@ -0,0 +1,268 @@
+module ActiveRecord
+ # :stopdoc:
+ module ConnectionAdapters
+ # An abstract definition of a column in a table.
+ class Column
+ TRUE_VALUES = [true, 1, '1', 't', 'T', 'true', 'TRUE'].to_set
+ FALSE_VALUES = [false, 0, '0', 'f', 'F', 'false', 'FALSE'].to_set
+
+ module Format
+ ISO_DATE = /\A(\d{4})-(\d\d)-(\d\d)\z/
+ ISO_DATETIME = /\A(\d{4})-(\d\d)-(\d\d) (\d\d):(\d\d):(\d\d)(\.\d+)?\z/
+ end
+
+ attr_reader :name, :default, :type, :limit, :null, :sql_type, :precision, :scale
+ attr_accessor :primary, :coder
+
+ alias :encoded? :coder
+
+ # Instantiates a new column in the table.
+ #
+ # +name+ is the column's name, such as <tt>supplier_id</tt> in <tt>supplier_id int(11)</tt>.
+ # +default+ is the type-casted default value, such as +new+ in <tt>sales_stage varchar(20) default 'new'</tt>.
+ # +sql_type+ is used to extract the column's length, if necessary. For example +60+ in
+ # <tt>company_name varchar(60)</tt>.
+ # It will be mapped to one of the standard Rails SQL types in the <tt>type</tt> attribute.
+ # +null+ determines if this column allows +NULL+ values.
+ def initialize(name, default, sql_type = nil, null = true)
+ @name = name
+ @sql_type = sql_type
+ @null = null
+ @limit = extract_limit(sql_type)
+ @precision = extract_precision(sql_type)
+ @scale = extract_scale(sql_type)
+ @type = simplified_type(sql_type)
+ @default = extract_default(default)
+ @primary = nil
+ @coder = nil
+ end
+
+ # Returns +true+ if the column is either of type string or text.
+ def text?
+ type == :string || type == :text
+ end
+
+ # Returns +true+ if the column is either of type integer, float or decimal.
+ def number?
+ type == :integer || type == :float || type == :decimal
+ end
+
+ def has_default?
+ !default.nil?
+ end
+
+ # Returns the Ruby class that corresponds to the abstract data type.
+ def klass
+ case type
+ when :integer then Fixnum
+ when :float then Float
+ when :decimal then BigDecimal
+ when :datetime, :timestamp, :time then Time
+ when :date then Date
+ when :text, :string, :binary then String
+ when :boolean then Object
+ end
+ end
+
+ # Casts value (which is a String) to an appropriate instance.
+ def type_cast(value)
+ return nil if value.nil?
+ return coder.load(value) if encoded?
+
+ klass = self.class
+
+ case type
+ when :string, :text then value
+ when :integer then value.to_i rescue value ? 1 : 0
+ when :float then value.to_f
+ when :decimal then klass.value_to_decimal(value)
+ when :datetime, :timestamp then klass.string_to_time(value)
+ when :time then klass.string_to_dummy_time(value)
+ when :date then klass.string_to_date(value)
+ when :binary then klass.binary_to_string(value)
+ when :boolean then klass.value_to_boolean(value)
+ else value
+ end
+ end
+
+ def type_cast_code(var_name)
+ klass = self.class.name
+
+ case type
+ when :string, :text then var_name
+ when :integer then "(#{var_name}.to_i rescue #{var_name} ? 1 : 0)"
+ when :float then "#{var_name}.to_f"
+ when :decimal then "#{klass}.value_to_decimal(#{var_name})"
+ when :datetime, :timestamp then "#{klass}.string_to_time(#{var_name})"
+ when :time then "#{klass}.string_to_dummy_time(#{var_name})"
+ when :date then "#{klass}.string_to_date(#{var_name})"
+ when :binary then "#{klass}.binary_to_string(#{var_name})"
+ when :boolean then "#{klass}.value_to_boolean(#{var_name})"
+ else var_name
+ end
+ end
+
+ # Returns the human name of the column name.
+ #
+ # ===== Examples
+ # Column.new('sales_stage', ...).human_name # => 'Sales stage'
+ def human_name
+ Base.human_attribute_name(@name)
+ end
+
+ def extract_default(default)
+ type_cast(default)
+ end
+
+ # Used to convert from Strings to BLOBs
+ def string_to_binary(value)
+ self.class.string_to_binary(value)
+ end
+
+ class << self
+ # Used to convert from Strings to BLOBs
+ def string_to_binary(value)
+ value
+ end
+
+ # Used to convert from BLOBs to Strings
+ def binary_to_string(value)
+ value
+ end
+
+ def string_to_date(string)
+ return string unless string.is_a?(String)
+ return nil if string.empty?
+
+ fast_string_to_date(string) || fallback_string_to_date(string)
+ end
+
+ def string_to_time(string)
+ return string unless string.is_a?(String)
+ return nil if string.empty?
+
+ fast_string_to_time(string) || fallback_string_to_time(string)
+ end
+
+ def string_to_dummy_time(string)
+ return string unless string.is_a?(String)
+ return nil if string.empty?
+
+ string_to_time "2000-01-01 #{string}"
+ end
+
+ # convert something to a boolean
+ def value_to_boolean(value)
+ if value.is_a?(String) && value.blank?
+ nil
+ else
+ TRUE_VALUES.include?(value)
+ end
+ end
+
+ # convert something to a BigDecimal
+ def value_to_decimal(value)
+ # Using .class is faster than .is_a? and
+ # subclasses of BigDecimal will be handled
+ # in the else clause
+ if value.class == BigDecimal
+ value
+ elsif value.respond_to?(:to_d)
+ value.to_d
+ else
+ value.to_s.to_d
+ end
+ end
+
+ protected
+ # '0.123456' -> 123456
+ # '1.123456' -> 123456
+ def microseconds(time)
+ ((time[:sec_fraction].to_f % 1) * 1_000_000).to_i
+ end
+
+ def new_date(year, mon, mday)
+ if year && year != 0
+ Date.new(year, mon, mday) rescue nil
+ end
+ end
+
+ def new_time(year, mon, mday, hour, min, sec, microsec)
+ # Treat 0000-00-00 00:00:00 as nil.
+ return nil if year.nil? || year == 0
+
+ Time.time_with_datetime_fallback(Base.default_timezone, year, mon, mday, hour, min, sec, microsec) rescue nil
+ end
+
+ def fast_string_to_date(string)
+ if string =~ Format::ISO_DATE
+ new_date $1.to_i, $2.to_i, $3.to_i
+ end
+ end
+
+ # Doesn't handle time zones.
+ def fast_string_to_time(string)
+ if string =~ Format::ISO_DATETIME
+ microsec = ($7.to_f * 1_000_000).to_i
+ new_time $1.to_i, $2.to_i, $3.to_i, $4.to_i, $5.to_i, $6.to_i, microsec
+ end
+ end
+
+ def fallback_string_to_date(string)
+ new_date(*::Date._parse(string, false).values_at(:year, :mon, :mday))
+ end
+
+ def fallback_string_to_time(string)
+ time_hash = Date._parse(string)
+ time_hash[:sec_fraction] = microseconds(time_hash)
+
+ new_time(*time_hash.values_at(:year, :mon, :mday, :hour, :min, :sec, :sec_fraction))
+ end
+ end
+
+ private
+ def extract_limit(sql_type)
+ $1.to_i if sql_type =~ /\((.*)\)/
+ end
+
+ def extract_precision(sql_type)
+ $2.to_i if sql_type =~ /^(numeric|decimal|number)\((\d+)(,\d+)?\)/i
+ end
+
+ def extract_scale(sql_type)
+ case sql_type
+ when /^(numeric|decimal|number)\((\d+)\)/i then 0
+ when /^(numeric|decimal|number)\((\d+)(,(\d+))\)/i then $4.to_i
+ end
+ end
+
+ def simplified_type(field_type)
+ case field_type
+ when /int/i
+ :integer
+ when /float|double/i
+ :float
+ when /decimal|numeric|number/i
+ extract_scale(field_type) == 0 ? :integer : :decimal
+ when /datetime/i
+ :datetime
+ when /timestamp/i
+ :timestamp
+ when /time/i
+ :time
+ when /date/i
+ :date
+ when /clob/i, /text/i
+ :text
+ when /blob/i, /binary/i
+ :binary
+ when /char/i, /string/i
+ :string
+ when /boolean/i
+ :boolean
+ end
+ end
+ end
+ end
+ # :startdoc:
+end
diff --git a/activerecord/lib/active_record/connection_adapters/mysql2_adapter.rb b/activerecord/lib/active_record/connection_adapters/mysql2_adapter.rb
index 568759775b..7bad511c64 100644
--- a/activerecord/lib/active_record/connection_adapters/mysql2_adapter.rb
+++ b/activerecord/lib/active_record/connection_adapters/mysql2_adapter.rb
@@ -1,11 +1,16 @@
# encoding: utf-8
-require 'mysql2' unless defined? Mysql2
+require 'mysql2'
module ActiveRecord
class Base
def self.mysql2_connection(config)
config[:username] = 'root' if config[:username].nil?
+
+ if Mysql2::Client.const_defined? :FOUND_ROWS
+ config[:flags] = Mysql2::Client::FOUND_ROWS
+ end
+
client = Mysql2::Client.new(config.symbolize_keys)
options = [config[:host], config[:username], config[:password], config[:database], config[:port], config[:socket], 0]
ConnectionAdapters::Mysql2Adapter.new(client, logger, options, config)
@@ -13,6 +18,9 @@ module ActiveRecord
end
module ConnectionAdapters
+ class Mysql2IndexDefinition < Struct.new(:table, :name, :unique, :columns, :lengths) #:nodoc:
+ end
+
class Mysql2Column < Column
BOOL = "tinyint(1)"
def extract_default(default)
@@ -34,62 +42,17 @@ module ActiveRecord
super
end
- # Returns the Ruby class that corresponds to the abstract data type.
- def klass
- case type
- when :integer then Fixnum
- when :float then Float
- when :decimal then BigDecimal
- when :datetime then Time
- when :date then Date
- when :timestamp then Time
- when :time then Time
- when :text, :string then String
- when :binary then String
- when :boolean then Object
- end
- end
-
- def type_cast(value)
- return nil if value.nil?
- case type
- when :string then value
- when :text then value
- when :integer then value.to_i rescue value ? 1 : 0
- when :float then value.to_f # returns self if it's already a Float
- when :decimal then self.class.value_to_decimal(value)
- when :datetime, :timestamp then value.class == Time ? value : self.class.string_to_time(value)
- when :time then value.class == Time ? value : self.class.string_to_dummy_time(value)
- when :date then value.class == Date ? value : self.class.string_to_date(value)
- when :binary then value
- when :boolean then self.class.value_to_boolean(value)
- else value
- end
- end
-
- def type_cast_code(var_name)
- case type
- when :string then nil
- when :text then nil
- when :integer then "#{var_name}.to_i rescue #{var_name} ? 1 : 0"
- when :float then "#{var_name}.to_f"
- when :decimal then "#{self.class.name}.value_to_decimal(#{var_name})"
- when :datetime, :timestamp then "#{var_name}.class == Time ? #{var_name} : #{self.class.name}.string_to_time(#{var_name})"
- when :time then "#{var_name}.class == Time ? #{var_name} : #{self.class.name}.string_to_dummy_time(#{var_name})"
- when :date then "#{var_name}.class == Date ? #{var_name} : #{self.class.name}.string_to_date(#{var_name})"
- when :binary then nil
- when :boolean then "#{self.class.name}.value_to_boolean(#{var_name})"
- else nil
- end
- end
-
private
def simplified_type(field_type)
return :boolean if Mysql2Adapter.emulate_booleans && field_type.downcase.index(BOOL)
- return :string if field_type =~ /enum/i or field_type =~ /set/i
- return :integer if field_type =~ /year/i
- return :binary if field_type =~ /bit/i
- super
+
+ case field_type
+ when /enum/i, /set/i then :string
+ when /year/i then :integer
+ when /bit/i then :binary
+ else
+ super
+ end
end
def extract_limit(sql_type)
@@ -234,10 +197,7 @@ module ActiveRecord
def active?
return false unless @connection
- @connection.query 'select 1'
- true
- rescue Mysql2::Error
- false
+ @connection.ping
end
def reconnect!
@@ -277,7 +237,7 @@ module ActiveRecord
# return r
# end
# end
- #
+ #
# # Returns a single value from a record
# def select_value(sql, name = nil)
# result = execute(sql, name)
@@ -285,7 +245,7 @@ module ActiveRecord
# first.first
# end
# end
- #
+ #
# # Returns an array of the values of the first column in a select:
# # select_values("SELECT id FROM companies LIMIT 3") => [1,2,3]
# def select_values(sql, name = nil)
@@ -368,6 +328,7 @@ module ActiveRecord
end
sql
end
+ deprecate :add_limit_offset!
# SCHEMA STATEMENTS ========================================
@@ -442,10 +403,11 @@ module ActiveRecord
if current_index != row[:Key_name]
next if row[:Key_name] == PRIMARY # skip the primary key
current_index = row[:Key_name]
- indexes << IndexDefinition.new(row[:Table], row[:Key_name], row[:Non_unique] == 0, [])
+ indexes << Mysql2IndexDefinition.new(row[:Table], row[:Key_name], row[:Non_unique] == 0, [], [])
end
indexes.last.columns << row[:Column_name]
+ indexes.last.lengths << row[:Sub_part]
end
indexes
end
@@ -453,7 +415,7 @@ module ActiveRecord
def columns(table_name, name = nil)
sql = "SHOW FIELDS FROM #{quote_table_name(table_name)}"
columns = []
- result = execute(sql, :skip_logging)
+ result = execute(sql)
result.each(:symbolize_keys => true, :as => :hash) { |field|
columns << Mysql2Column.new(field[:Field], field[:Default], field[:Type], field[:Null] == "YES")
}
@@ -606,12 +568,21 @@ module ActiveRecord
def configure_connection
@connection.query_options.merge!(:as => :array)
- encoding = @config[:encoding]
- execute("SET NAMES '#{encoding}'", :skip_logging) if encoding
# By default, MySQL 'where id is null' selects the last inserted id.
# Turn this off. http://dev.rubyonrails.org/ticket/6778
- execute("SET SQL_AUTO_IS_NULL=0", :skip_logging)
+ variable_assignments = ['SQL_AUTO_IS_NULL=0']
+ encoding = @config[:encoding]
+
+ # make sure we set the encoding
+ variable_assignments << "NAMES '#{encoding}'" if encoding
+
+ # increase timeout so mysql server doesn't disconnect us
+ wait_timeout = @config[:wait_timeout]
+ wait_timeout = 2592000 unless wait_timeout.is_a?(Fixnum)
+ variable_assignments << "@@wait_timeout = #{wait_timeout}"
+
+ execute("SET #{variable_assignments.join(', ')}", :skip_logging)
end
# Returns an array of record hashes with the column names as keys and
diff --git a/activerecord/lib/active_record/connection_adapters/mysql_adapter.rb b/activerecord/lib/active_record/connection_adapters/mysql_adapter.rb
index ba0051de05..e1186209d3 100644
--- a/activerecord/lib/active_record/connection_adapters/mysql_adapter.rb
+++ b/activerecord/lib/active_record/connection_adapters/mysql_adapter.rb
@@ -3,6 +3,28 @@ require 'active_support/core_ext/kernel/requires'
require 'active_support/core_ext/object/blank'
require 'set'
+begin
+ require 'mysql'
+rescue LoadError
+ raise "!!! Missing the mysql gem. Add it to your Gemfile: gem 'mysql'"
+end
+
+unless defined?(Mysql::Result) && Mysql::Result.method_defined?(:each_hash)
+ raise "!!! Outdated mysql gem. Upgrade to 2.8.1 or later. In your Gemfile: gem 'mysql', '2.8.1'. Or use gem 'mysql2'"
+end
+
+class Mysql
+ class Time
+ ###
+ # This monkey patch is for test_additional_columns_from_join_table
+ def to_date
+ Date.new(year, month, day)
+ end
+ end
+ class Stmt; include Enumerable end
+ class Result; include Enumerable end
+end
+
module ActiveRecord
class Base
# Establishes a connection to the database that's used by all Active Record objects.
@@ -15,22 +37,11 @@ module ActiveRecord
password = config[:password].to_s
database = config[:database]
- unless defined? Mysql
- begin
- require 'mysql'
- rescue LoadError
- raise "!!! Missing the mysql gem. Add it to your Gemfile: gem 'mysql', '2.8.1'"
- end
-
- unless defined?(Mysql::Result) && Mysql::Result.method_defined?(:each_hash)
- raise "!!! Outdated mysql gem. Upgrade to 2.8.1 or later. In your Gemfile: gem 'mysql', '2.8.1'"
- end
- end
-
mysql = Mysql.init
mysql.ssl_set(config[:sslkey], config[:sslcert], config[:sslca], config[:sslcapath], config[:sslcipher]) if config[:sslca] || config[:sslkey]
default_flags = Mysql.const_defined?(:CLIENT_MULTI_RESULTS) ? Mysql::CLIENT_MULTI_RESULTS : 0
+ default_flags |= Mysql::CLIENT_FOUND_ROWS if Mysql.const_defined?(:CLIENT_FOUND_ROWS)
options = [host, username, password, database, port, socket, default_flags]
ConnectionAdapters::MysqlAdapter.new(mysql, logger, options, config)
end
@@ -38,6 +49,30 @@ module ActiveRecord
module ConnectionAdapters
class MysqlColumn < Column #:nodoc:
+ class << self
+ def string_to_time(value)
+ return super unless Mysql::Time === value
+ new_time(
+ value.year,
+ value.month,
+ value.day,
+ value.hour,
+ value.minute,
+ value.second,
+ value.second_part)
+ end
+
+ def string_to_dummy_time(v)
+ return super unless Mysql::Time === v
+ new_time(2000, 01, 01, v.hour, v.minute, v.second, v.second_part)
+ end
+
+ def string_to_date(v)
+ return super unless Mysql::Time === v
+ new_date(v.year, v.month, v.day)
+ end
+ end
+
def extract_default(default)
if sql_type =~ /blob/i || type == :text
if default.blank?
@@ -131,7 +166,7 @@ module ActiveRecord
cattr_accessor :emulate_booleans
self.emulate_booleans = true
- ADAPTER_NAME = 'MySQL'.freeze
+ ADAPTER_NAME = 'MySQL'
LOST_CONNECTION_ERROR_MESSAGES = [
"Server shutdown in progress",
@@ -139,10 +174,10 @@ module ActiveRecord
"Lost connection to MySQL server during query",
"MySQL server has gone away" ]
- QUOTED_TRUE, QUOTED_FALSE = '1'.freeze, '0'.freeze
+ QUOTED_TRUE, QUOTED_FALSE = '1', '0'
NATIVE_DATABASE_TYPES = {
- :primary_key => "int(11) DEFAULT NULL auto_increment PRIMARY KEY".freeze,
+ :primary_key => "int(11) DEFAULT NULL auto_increment PRIMARY KEY",
:string => { :name => "varchar", :limit => 255 },
:text => { :name => "text" },
:integer => { :name => "int", :limit => 4 },
@@ -160,6 +195,7 @@ module ActiveRecord
super(connection, logger)
@connection_options, @config = connection_options, config
@quoted_column_names, @quoted_table_names = {}, {}
+ @statements = {}
connect
end
@@ -167,6 +203,16 @@ module ActiveRecord
ADAPTER_NAME
end
+ def supports_bulk_alter? #:nodoc:
+ true
+ end
+
+ # Returns +true+ when the connection adapter supports prepared statement
+ # caching, otherwise returns +false+
+ def supports_statement_cache?
+ true
+ end
+
def supports_migrations? #:nodoc:
true
end
@@ -251,6 +297,7 @@ module ActiveRecord
def reconnect!
disconnect!
+ clear_cache!
connect
end
@@ -271,14 +318,69 @@ module ActiveRecord
def select_rows(sql, name = nil)
@connection.query_with_result = true
- result = execute(sql, name)
- rows = []
- result.each { |row| rows << row }
- result.free
+ rows = exec_without_stmt(sql, name).rows
+ @connection.more_results && @connection.next_result # invoking stored procedures with CLIENT_MULTI_RESULTS requires this to tidy up else connection will be dropped
rows
end
- # Executes an SQL query and returns a MySQL::Result object. Note that you have to free
+ def clear_cache!
+ @statements.values.each do |cache|
+ cache[:stmt].close
+ end
+ @statements.clear
+ end
+
+ def exec_query(sql, name = 'SQL', binds = [])
+ log(sql, name, binds) do
+ result = nil
+
+ cache = {}
+ if binds.empty?
+ stmt = @connection.prepare(sql)
+ else
+ cache = @statements[sql] ||= {
+ :stmt => @connection.prepare(sql)
+ }
+ stmt = cache[:stmt]
+ end
+
+ stmt.execute(*binds.map { |col, val|
+ col ? col.type_cast(val) : val
+ })
+ if metadata = stmt.result_metadata
+ cols = cache[:cols] ||= metadata.fetch_fields.map { |field|
+ field.name
+ }
+
+ metadata.free
+ result = ActiveRecord::Result.new(cols, stmt.to_a)
+ end
+
+ stmt.free_result
+ stmt.close if binds.empty?
+
+ result
+ end
+ end
+
+ def exec_without_stmt(sql, name = 'SQL') # :nodoc:
+ # Some queries, like SHOW CREATE TABLE don't work through the prepared
+ # statement API. For those queries, we need to use this method. :'(
+ log(sql, name) do
+ result = @connection.query(sql)
+ cols = []
+ rows = []
+
+ if result
+ cols = result.fetch_fields.map { |field| field.name }
+ rows = result.to_a
+ result.free
+ end
+ ActiveRecord::Result.new(cols, rows)
+ end
+ end
+
+ # Executes an SQL query and returns a MySQL::Result object. Note that you have to free
# the Result object after you're done using it.
def execute(sql, name = nil) #:nodoc:
if name == :skip_logging
@@ -306,8 +408,8 @@ module ActiveRecord
end
def begin_db_transaction #:nodoc:
- execute "BEGIN"
- rescue Exception
+ exec_without_stmt "BEGIN"
+ rescue Mysql::Error
# Transactions aren't supported
end
@@ -346,6 +448,7 @@ module ActiveRecord
end
sql
end
+ deprecate :add_limit_offset!
# SCHEMA STATEMENTS ========================================
@@ -356,10 +459,11 @@ module ActiveRecord
sql = "SHOW TABLES"
end
- select_all(sql).inject("") do |structure, table|
+ select_all(sql).map do |table|
table.delete('Table_type')
- structure += select_one("SHOW CREATE TABLE #{quote_table_name(table.to_a.first.last)}")["Create Table"] + ";\n\n"
- end
+ sql = "SHOW CREATE TABLE #{quote_table_name(table.to_a.first.last)}"
+ exec_without_stmt(sql).first['Create Table'] + ";\n\n"
+ end.join("")
end
def recreate_database(name, options = {}) #:nodoc:
@@ -400,14 +504,28 @@ module ActiveRecord
show_variable 'collation_database'
end
- def tables(name = nil) #:nodoc:
+ def tables(name = nil, database = nil) #:nodoc:
tables = []
- result = execute("SHOW TABLES", name)
+ result = execute(["SHOW TABLES", database].compact.join(' IN '), name)
result.each { |field| tables << field[0] }
result.free
tables
end
+ def table_exists?(name)
+ return true if super
+
+ name = name.to_s
+ schema, table = name.split('.', 2)
+
+ unless table # A table was provided without a schema
+ table = schema
+ schema = nil
+ end
+
+ tables(nil, schema).include? table
+ end
+
def drop_table(table_name, options = {})
super(table_name, options)
end
@@ -433,7 +551,7 @@ module ActiveRecord
def columns(table_name, name = nil)#:nodoc:
sql = "SHOW FIELDS FROM #{quote_table_name(table_name)}"
columns = []
- result = execute(sql, :skip_logging)
+ result = execute(sql)
result.each { |field| columns << MysqlColumn.new(field[0], field[4], field[1], field[2] == "YES") }
result.free
columns
@@ -447,11 +565,23 @@ module ActiveRecord
execute "RENAME TABLE #{quote_table_name(table_name)} TO #{quote_table_name(new_name)}"
end
+ def bulk_change_table(table_name, operations) #:nodoc:
+ sqls = operations.map do |command, args|
+ table, arguments = args.shift, args
+ method = :"#{command}_sql"
+
+ if respond_to?(method)
+ send(method, table, *arguments)
+ else
+ raise "Unknown method called : #{method}(#{arguments.inspect})"
+ end
+ end.flatten.join(", ")
+
+ execute("ALTER TABLE #{quote_table_name(table_name)} #{sqls}")
+ end
+
def add_column(table_name, column_name, type, options = {})
- add_column_sql = "ALTER TABLE #{quote_table_name(table_name)} ADD #{quote_column_name(column_name)} #{type_to_sql(type, options[:limit], options[:precision], options[:scale])}"
- add_column_options!(add_column_sql, options)
- add_column_position!(add_column_sql, options)
- execute(add_column_sql)
+ execute("ALTER TABLE #{quote_table_name(table_name)} #{add_column_sql(table_name, column_name, type, options)}")
end
def change_column_default(table_name, column_name, default) #:nodoc:
@@ -470,34 +600,11 @@ module ActiveRecord
end
def change_column(table_name, column_name, type, options = {}) #:nodoc:
- column = column_for(table_name, column_name)
-
- unless options_include_default?(options)
- options[:default] = column.default
- end
-
- unless options.has_key?(:null)
- options[:null] = column.null
- end
-
- change_column_sql = "ALTER TABLE #{quote_table_name(table_name)} CHANGE #{quote_column_name(column_name)} #{quote_column_name(column_name)} #{type_to_sql(type, options[:limit], options[:precision], options[:scale])}"
- add_column_options!(change_column_sql, options)
- add_column_position!(change_column_sql, options)
- execute(change_column_sql)
+ execute("ALTER TABLE #{quote_table_name(table_name)} #{change_column_sql(table_name, column_name, type, options)}")
end
def rename_column(table_name, column_name, new_column_name) #:nodoc:
- options = {}
- if column = columns(table_name).find { |c| c.name == column_name.to_s }
- options[:default] = column.default
- options[:null] = column.null
- else
- raise ActiveRecordError, "No such column: #{table_name}.#{column_name}"
- end
- current_type = select_one("SHOW COLUMNS FROM #{quote_table_name(table_name)} LIKE '#{column_name}'")["Type"]
- rename_column_sql = "ALTER TABLE #{quote_table_name(table_name)} CHANGE #{quote_column_name(column_name)} #{quote_column_name(new_column_name)} #{current_type}"
- add_column_options!(rename_column_sql, options)
- execute(rename_column_sql)
+ execute("ALTER TABLE #{quote_table_name(table_name)} #{rename_column_sql(table_name, column_name, new_column_name)}")
end
# Maps logical Rails types to MySQL-specific data types.
@@ -580,6 +687,69 @@ module ActiveRecord
end
end
+ def add_column_sql(table_name, column_name, type, options = {})
+ add_column_sql = "ADD #{quote_column_name(column_name)} #{type_to_sql(type, options[:limit], options[:precision], options[:scale])}"
+ add_column_options!(add_column_sql, options)
+ add_column_position!(add_column_sql, options)
+ add_column_sql
+ end
+
+ def remove_column_sql(table_name, *column_names)
+ columns_for_remove(table_name, *column_names).map {|column_name| "DROP #{column_name}" }
+ end
+ alias :remove_columns_sql :remove_column
+
+ def change_column_sql(table_name, column_name, type, options = {})
+ column = column_for(table_name, column_name)
+
+ unless options_include_default?(options)
+ options[:default] = column.default
+ end
+
+ unless options.has_key?(:null)
+ options[:null] = column.null
+ end
+
+ change_column_sql = "CHANGE #{quote_column_name(column_name)} #{quote_column_name(column_name)} #{type_to_sql(type, options[:limit], options[:precision], options[:scale])}"
+ add_column_options!(change_column_sql, options)
+ add_column_position!(change_column_sql, options)
+ change_column_sql
+ end
+
+ def rename_column_sql(table_name, column_name, new_column_name)
+ options = {}
+
+ if column = columns(table_name).find { |c| c.name == column_name.to_s }
+ options[:default] = column.default
+ options[:null] = column.null
+ else
+ raise ActiveRecordError, "No such column: #{table_name}.#{column_name}"
+ end
+
+ current_type = select_one("SHOW COLUMNS FROM #{quote_table_name(table_name)} LIKE '#{column_name}'")["Type"]
+ rename_column_sql = "CHANGE #{quote_column_name(column_name)} #{quote_column_name(new_column_name)} #{current_type}"
+ add_column_options!(rename_column_sql, options)
+ rename_column_sql
+ end
+
+ def add_index_sql(table_name, column_name, options = {})
+ index_name, index_type, index_columns = add_index_options(table_name, column_name, options)
+ "ADD #{index_type} INDEX #{index_name} (#{index_columns})"
+ end
+
+ def remove_index_sql(table_name, options = {})
+ index_name = index_name_for_remove(table_name, options)
+ "DROP INDEX #{index_name}"
+ end
+
+ def add_timestamps_sql(table_name)
+ [add_column_sql(table_name, :created_at, :datetime), add_column_sql(table_name, :updated_at, :datetime)]
+ end
+
+ def remove_timestamps_sql(table_name)
+ [remove_column_sql(table_name, :updated_at), remove_column_sql(table_name, :created_at)]
+ end
+
private
def connect
encoding = @config[:encoding]
@@ -612,12 +782,10 @@ module ActiveRecord
execute("SET SQL_AUTO_IS_NULL=0", :skip_logging)
end
- def select(sql, name = nil)
+ def select(sql, name = nil, binds = [])
@connection.query_with_result = true
- result = execute(sql, name)
- rows = []
- result.each_hash { |row| rows << row }
- result.free
+ rows = exec_query(sql, name, binds).to_a
+ @connection.more_results && @connection.next_result # invoking stored procedures with CLIENT_MULTI_RESULTS requires this to tidy up else connection will be dropped
rows
end
diff --git a/activerecord/lib/active_record/connection_adapters/postgresql_adapter.rb b/activerecord/lib/active_record/connection_adapters/postgresql_adapter.rb
index 6fae899e87..5a830a50fb 100644
--- a/activerecord/lib/active_record/connection_adapters/postgresql_adapter.rb
+++ b/activerecord/lib/active_record/connection_adapters/postgresql_adapter.rb
@@ -1,20 +1,19 @@
require 'active_record/connection_adapters/abstract_adapter'
require 'active_support/core_ext/kernel/requires'
require 'active_support/core_ext/object/blank'
+require 'pg'
module ActiveRecord
class Base
# Establishes a connection to the database that's used by all Active Record objects
def self.postgresql_connection(config) # :nodoc:
- require 'pg'
-
config = config.symbolize_keys
host = config[:host]
port = config[:port] || 5432
username = config[:username].to_s if config[:username]
password = config[:password].to_s if config[:password]
- if config.has_key?(:database)
+ if config.key?(:database)
database = config[:database]
else
raise ArgumentError, "No database specified. Missing argument: database."
@@ -27,12 +26,6 @@ module ActiveRecord
end
module ConnectionAdapters
- class TableDefinition
- def xml(*args)
- options = args.extract_options!
- column(args[0], 'xml', options)
- end
- end
# PostgreSQL-specific extensions to column definitions in a table.
class PostgreSQLColumn < Column #:nodoc:
# Instantiates a new PostgreSQL column definition in a table.
@@ -170,9 +163,7 @@ module ActiveRecord
end
end
end
- end
- module ConnectionAdapters
# The PostgreSQL adapter works both with the native C (http://ruby.scripting.ca/postgres/) and the pure
# Ruby (available both as gem and from http://rubyforge.org/frs/?group_id=234&release_id=1944) drivers.
#
@@ -183,19 +174,24 @@ module ActiveRecord
# * <tt>:username</tt> - Defaults to nothing.
# * <tt>:password</tt> - Defaults to nothing.
# * <tt>:database</tt> - The name of the database. No default, must be provided.
- # * <tt>:schema_search_path</tt> - An optional schema search path for the connection given
+ # * <tt>:schema_search_path</tt> - An optional schema search path for the connection given
# as a string of comma-separated schema names. This is backward-compatible with the <tt>:schema_order</tt> option.
- # * <tt>:encoding</tt> - An optional client encoding that is used in a <tt>SET client_encoding TO
+ # * <tt>:encoding</tt> - An optional client encoding that is used in a <tt>SET client_encoding TO
# <encoding></tt> call on the connection.
- # * <tt>:min_messages</tt> - An optional client min messages that is used in a
+ # * <tt>:min_messages</tt> - An optional client min messages that is used in a
# <tt>SET client_min_messages TO <min_messages></tt> call on the connection.
- # * <tt>:allow_concurrency</tt> - If true, use async query methods so Ruby threads don't deadlock;
- # otherwise, use blocking query methods.
class PostgreSQLAdapter < AbstractAdapter
- ADAPTER_NAME = 'PostgreSQL'.freeze
+ class TableDefinition < ActiveRecord::ConnectionAdapters::TableDefinition
+ def xml(*args)
+ options = args.extract_options!
+ column(args[0], 'xml', options)
+ end
+ end
+
+ ADAPTER_NAME = 'PostgreSQL'
NATIVE_DATABASE_TYPES = {
- :primary_key => "serial primary key".freeze,
+ :primary_key => "serial primary key",
:string => { :name => "character varying", :limit => 255 },
:text => { :name => "text" },
:integer => { :name => "integer" },
@@ -215,6 +211,12 @@ module ActiveRecord
ADAPTER_NAME
end
+ # Returns +true+ when the connection adapter supports prepared statement
+ # caching, otherwise returns +false+
+ def supports_statement_cache?
+ true
+ end
+
# Initializes and connects a PostgreSQL adapter.
def initialize(connection, logger, connection_parameters, config)
super(connection, logger)
@@ -224,11 +226,19 @@ module ActiveRecord
@local_tz = nil
@table_alias_length = nil
@postgresql_version = nil
+ @statements = {}
connect
@local_tz = execute('SHOW TIME ZONE').first["TimeZone"]
end
+ def clear_cache!
+ @statements.each_value do |value|
+ @connection.query "DEALLOCATE #{value}"
+ end
+ @statements.clear
+ end
+
# Is this connection alive and ready for queries?
def active?
if @connection.respond_to?(:status)
@@ -246,6 +256,7 @@ module ActiveRecord
# Close then reopen the connection.
def reconnect!
if @connection.respond_to?(:reset)
+ clear_cache!
@connection.reset
configure_connection
else
@@ -254,8 +265,14 @@ module ActiveRecord
end
end
+ def reset!
+ clear_cache!
+ super
+ end
+
# Close the connection.
def disconnect!
+ clear_cache!
@connection.close rescue nil
end
@@ -317,19 +334,22 @@ module ActiveRecord
def quote(value, column = nil) #:nodoc:
return super unless column
- if value.kind_of?(String) && column.type == :binary
- "'#{escape_bytea(value)}'"
- elsif value.kind_of?(String) && column.sql_type == 'xml'
- "xml '#{quote_string(value)}'"
- elsif value.kind_of?(Numeric) && column.sql_type == 'money'
+ case value
+ when Numeric
+ return super unless column.sql_type == 'money'
# Not truly string input, so doesn't require (or allow) escape string syntax.
"'#{value}'"
- elsif value.kind_of?(String) && column.sql_type =~ /^bit/
- case value
- when /^[01]*$/
- "B'#{value}'" # Bit-string notation
- when /^[0-9A-F]*$/i
- "X'#{value}'" # Hexadecimal notation
+ when String
+ case column.sql_type
+ when 'bytea' then "'#{escape_bytea(value)}'"
+ when 'xml' then "xml '#{quote_string(value)}'"
+ when /^bit/
+ case value
+ when /^[01]*$/ then "B'#{value}'" # Bit-string notation
+ when /^[0-9A-F]*$/i then "X'#{value}'" # Hexadecimal notation
+ end
+ else
+ super
end
else
super
@@ -375,13 +395,16 @@ module ActiveRecord
end
end
+ # Set the authorized user for this session
+ def session_auth=(user)
+ clear_cache!
+ exec_query "SET SESSION AUTHORIZATION #{user}"
+ end
+
# REFERENTIAL INTEGRITY ====================================
def supports_disable_referential_integrity?() #:nodoc:
- version = query("SHOW server_version")[0][0].split('.')
- version[0].to_i >= 8 && version[1].to_i >= 1
- rescue
- return false
+ postgresql_version >= 80100
end
def disable_referential_integrity #:nodoc:
@@ -430,7 +453,7 @@ module ActiveRecord
# If a pk is given, fallback to default sequence name.
# Don't fetch last insert id for a table without a pk.
if pk && sequence_name ||= default_sequence_name(table, pk)
- last_insert_id(table, sequence_name)
+ last_insert_id(sequence_name)
end
end
end
@@ -466,8 +489,8 @@ module ActiveRecord
# (2) $12.345.678,12
case data
when /^-?\D+[\d,]+\.\d{2}$/ # (1)
- data.gsub!(/[^-\d\.]/, '')
- when /^-?\D+[\d\.]+,\d{2}$/ # (2)
+ data.gsub!(/[^-\d.]/, '')
+ when /^-?\D+[\d.]+,\d{2}$/ # (2)
data.gsub!(/[^-\d,]/, '').sub!(/,/, '.')
end
end
@@ -502,6 +525,35 @@ module ActiveRecord
end
end
+ def substitute_for(column, current_values)
+ Arel.sql("$#{current_values.length + 1}")
+ end
+
+ def exec_query(sql, name = 'SQL', binds = [])
+ return exec_no_cache(sql, name) if binds.empty?
+
+ log(sql, name, binds) do
+ unless @statements.key? sql
+ nextkey = "a#{@statements.length + 1}"
+ @connection.prepare nextkey, sql
+ @statements[sql] = nextkey
+ end
+
+ key = @statements[sql]
+
+ # Clear the queue
+ @connection.get_last_result
+ @connection.send_query_prepared(key, binds.map { |col, val|
+ col ? col.type_cast(val) : val
+ })
+ @connection.block
+ result = @connection.get_last_result
+ ret = ActiveRecord::Result.new(result.fields, result_as_array(result))
+ result.clear
+ return ret
+ end
+ end
+
# Executes an UPDATE query and returns the number of affected tuples.
def update_sql(sql, name = nil)
super.cmd_tuples
@@ -659,8 +711,8 @@ module ActiveRecord
# Returns the list of all column definitions for a table.
def columns(table_name, name = nil)
# Limit, precision, and scale are all handled by the superclass.
- column_definitions(table_name).collect do |name, type, default, notnull|
- PostgreSQLColumn.new(name, default, type, notnull == 'f')
+ column_definitions(table_name).collect do |column_name, type, default, notnull|
+ PostgreSQLColumn.new(column_name, default, type, notnull == 'f')
end
end
@@ -734,7 +786,7 @@ module ActiveRecord
def pk_and_sequence_for(table) #:nodoc:
# First try looking for a sequence with a dependency on the
# given table's primary key.
- result = query(<<-end_sql, 'PK and serial sequence')[0]
+ result = exec_query(<<-end_sql, 'PK and serial sequence').rows.first
SELECT attr.attname, seq.relname
FROM pg_class seq,
pg_attribute attr,
@@ -793,14 +845,18 @@ module ActiveRecord
# Adds a new column to the named table.
# See TableDefinition#column for details of the options you can use.
def add_column(table_name, column_name, type, options = {})
- default = options[:default]
- notnull = options[:null] == false
+ add_column_sql = "ALTER TABLE #{quote_table_name(table_name)} ADD COLUMN #{quote_column_name(column_name)} #{type_to_sql(type, options[:limit], options[:precision], options[:scale])}"
+ add_column_options!(add_column_sql, options)
- # Add the column.
- execute("ALTER TABLE #{quote_table_name(table_name)} ADD COLUMN #{quote_column_name(column_name)} #{type_to_sql(type, options[:limit], options[:precision], options[:scale])}")
+ begin
+ execute add_column_sql
+ rescue ActiveRecord::StatementInvalid => e
+ raise e if postgresql_version > 80000
- change_column_default(table_name, column_name, default) if options_include_default?(options)
- change_column_null(table_name, column_name, false, default) if notnull
+ execute("ALTER TABLE #{quote_table_name(table_name)} ADD COLUMN #{quote_column_name(column_name)} #{type_to_sql(type, options[:limit], options[:precision], options[:scale])}")
+ change_column_default(table_name, column_name, options[:default]) if options_include_default?(options)
+ change_column_null(table_name, column_name, options[:null], options[:default]) if options.key?(:null)
+ end
end
# Changes the column of a table.
@@ -850,6 +906,10 @@ module ActiveRecord
execute "DROP INDEX #{quote_table_name(index_name)}"
end
+ def rename_index(table_name, old_name, new_name)
+ execute "ALTER INDEX #{quote_column_name(old_name)} RENAME TO #{quote_table_name(new_name)}"
+ end
+
def index_name_length
63
end
@@ -873,13 +933,13 @@ module ActiveRecord
# requires that the ORDER BY include the distinct column.
#
# distinct("posts.id", "posts.created_at desc")
- def distinct(columns, order_by) #:nodoc:
- return "DISTINCT #{columns}" if order_by.blank?
+ def distinct(columns, orders) #:nodoc:
+ return "DISTINCT #{columns}" if orders.empty?
# Construct a clean list of column names from the ORDER BY clause, removing
# any ASC/DESC modifiers
- order_columns = order_by.split(',').collect { |s| s.split.first }
- order_columns.delete_if(&:blank?)
+ order_columns = orders.collect { |s| s =~ /^(.+)\s+(ASC|DESC)\s*$/i ? $1 : s }
+ order_columns.delete_if { |c| c.blank? }
order_columns = order_columns.zip((0...order_columns.size).to_a).map { |s,i| "#{s} AS alias_#{i}" }
# Return a DISTINCT ON() clause that's distinct on the columns we want but includes
@@ -897,8 +957,12 @@ module ActiveRecord
else
# Mimic PGconn.server_version behavior
begin
- query('SELECT version()')[0][0] =~ /PostgreSQL (\d+)\.(\d+)\.(\d+)/
- ($1.to_i * 10000) + ($2.to_i * 100) + $3.to_i
+ if query('SELECT version()')[0][0] =~ /PostgreSQL ([0-9.]+)/
+ major, minor, tiny = $1.split(".")
+ (major.to_i * 10000) + (minor.to_i * 100) + tiny.to_i
+ else
+ 0
+ end
rescue
0
end
@@ -917,6 +981,15 @@ module ActiveRecord
end
private
+ def exec_no_cache(sql, name)
+ log(sql, name) do
+ result = @connection.async_exec(sql)
+ ret = ActiveRecord::Result.new(result.fields, result_as_array(result))
+ result.clear
+ ret
+ end
+ end
+
# The internal PostgreSQL identifier of the money data type.
MONEY_COLUMN_TYPE_OID = 790 #:nodoc:
# The internal PostgreSQL identifier of the BYTEA data type.
@@ -929,7 +1002,7 @@ module ActiveRecord
PGconn.translate_results = false if PGconn.respond_to?(:translate_results=)
# Ignore async_exec and async_query when using postgres-pr.
- @async = @config[:allow_concurrency] && @connection.respond_to?(:async_exec)
+ @async = @connection.respond_to?(:async_exec)
# Money type has a fixed precision of 10 in PostgreSQL 8.2 and below, and as of
# PostgreSQL 8.3 it has a fixed precision of 19. PostgreSQLColumn.extract_precision
@@ -965,17 +1038,15 @@ module ActiveRecord
end
# Returns the current ID of a table's sequence.
- def last_insert_id(table, sequence_name) #:nodoc:
- Integer(select_value("SELECT currval('#{sequence_name}')"))
+ def last_insert_id(sequence_name) #:nodoc:
+ r = exec_query("SELECT currval($1)", 'SQL', [[nil, sequence_name]])
+ Integer(r.rows.first.first)
end
# Executes a SELECT query and returns the results, performing any data type
# conversions that are required to be performed here instead of in PostgreSQLColumn.
- def select(sql, name = nil)
- fields, rows = select_raw(sql, name)
- rows.map do |row|
- Hash[*fields.zip(row).flatten]
- end
+ def select(sql, name = nil, binds = [])
+ exec_query(sql, name, binds).to_a
end
def select_raw(sql, name = nil)
@@ -1005,7 +1076,7 @@ module ActiveRecord
# - format_type includes the column size constraint, e.g. varchar(50)
# - ::regclass is a function that gives the id for a table name
def column_definitions(table_name) #:nodoc:
- query <<-end_sql
+ exec_query(<<-end_sql).rows
SELECT a.attname, format_type(a.atttypid, a.atttypmod), d.adsrc, a.attnotnull
FROM pg_attribute a LEFT JOIN pg_attrdef d
ON a.attrelid = d.adrelid AND a.attnum = d.adnum
@@ -1016,14 +1087,18 @@ module ActiveRecord
end
def extract_pg_identifier_from_name(name)
- match_data = name[0,1] == '"' ? name.match(/\"([^\"]+)\"/) : name.match(/([^\.]+)/)
+ match_data = name.start_with?('"') ? name.match(/\"([^\"]+)\"/) : name.match(/([^\.]+)/)
if match_data
- rest = name[match_data[0].length..-1]
- rest = rest[1..-1] if rest[0,1] == "."
+ rest = name[match_data[0].length, name.length]
+ rest = rest[1, rest.length] if rest.start_with? "."
[match_data[1], (rest.length > 0 ? rest : nil)]
end
end
+
+ def table_definition
+ TableDefinition.new(self)
+ 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 e5e92f2b1c..c3a7b039ff 100644
--- a/activerecord/lib/active_record/connection_adapters/sqlite3_adapter.rb
+++ b/activerecord/lib/active_record/connection_adapters/sqlite3_adapter.rb
@@ -1,4 +1,5 @@
require 'active_record/connection_adapters/sqlite_adapter'
+require 'sqlite3'
module ActiveRecord
class Base
@@ -20,16 +21,12 @@ module ActiveRecord
raise ArgumentError, 'adapter name should be "sqlite3"'
end
- unless self.class.const_defined?(:SQLite3)
- require_library_or_gem(config[:adapter])
- end
-
db = SQLite3::Database.new(
config[:database],
:results_as_hash => true
)
- db.busy_timeout(config[:timeout]) unless config[:timeout].nil?
+ db.busy_timeout(config[:timeout]) if config[:timeout]
ConnectionAdapters::SQLite3Adapter.new(db, logger, config)
end
@@ -37,14 +34,21 @@ module ActiveRecord
module ConnectionAdapters #:nodoc:
class SQLite3Adapter < SQLiteAdapter # :nodoc:
+ def quote(value, column = nil)
+ if value.kind_of?(String) && column && column.type == :binary && column.class.respond_to?(:string_to_binary)
+ s = column.class.string_to_binary(value).unpack("H*")[0]
+ "x'#{s}'"
+ else
+ super
+ end
+ end
# Returns the current database encoding format as a string, eg: 'UTF-8'
def encoding
if @connection.respond_to?(:encoding)
@connection.encoding.to_s
else
- encoding = @connection.execute('PRAGMA encoding')
- encoding[0]['encoding']
+ @connection.execute('PRAGMA encoding')[0]['encoding']
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 82ad0a3b8e..ae61d6ce94 100644
--- a/activerecord/lib/active_record/connection_adapters/sqlite_adapter.rb
+++ b/activerecord/lib/active_record/connection_adapters/sqlite_adapter.rb
@@ -29,7 +29,7 @@ module ActiveRecord
end
end
- # The SQLite adapter works with both the 2.x and 3.x series of SQLite with the sqlite-ruby
+ # The SQLite adapter works with both the 2.x and 3.x series of SQLite with the sqlite-ruby
# drivers (available both as gems and from http://rubyforge.org/projects/sqlite-ruby/).
#
# Options:
@@ -40,16 +40,17 @@ module ActiveRecord
include Comparable
def initialize(version_string)
- @version = version_string.split('.').map(&:to_i)
+ @version = version_string.split('.').map { |v| v.to_i }
end
def <=>(version_string)
- @version <=> version_string.split('.').map(&:to_i)
+ @version <=> version_string.split('.').map { |v| v.to_i }
end
end
def initialize(connection, logger, config)
super(connection, logger)
+ @statements = {}
@config = config
end
@@ -61,6 +62,16 @@ module ActiveRecord
sqlite_version >= '2.0.0'
end
+ def supports_savepoints?
+ sqlite_version >= '3.6.8'
+ end
+
+ # Returns +true+ when the connection adapter supports prepared statement
+ # caching, otherwise returns +false+
+ def supports_statement_cache?
+ true
+ end
+
def supports_migrations? #:nodoc:
true
end
@@ -79,9 +90,14 @@ module ActiveRecord
def disconnect!
super
+ clear_cache!
@connection.close rescue nil
end
+ def clear_cache!
+ @statements.clear
+ end
+
def supports_count_distinct? #:nodoc:
sqlite_version >= '3.2.6'
end
@@ -121,7 +137,7 @@ module ActiveRecord
# Quote date/time values for use in SQL input. Includes microseconds
# if the value is a Time responding to usec.
def quoted_date(value) #:nodoc:
- if value.acts_like?(:time) && value.respond_to?(:usec)
+ if value.respond_to?(:usec)
"#{super}.#{sprintf("%06d", value.usec)}"
else
super
@@ -131,6 +147,32 @@ module ActiveRecord
# DATABASE STATEMENTS ======================================
+ def exec_query(sql, name = nil, binds = [])
+ log(sql, name, binds) do
+
+ # Don't cache statements without bind values
+ if binds.empty?
+ stmt = @connection.prepare(sql)
+ cols = stmt.columns
+ records = stmt.to_a
+ stmt.close
+ stmt = records
+ else
+ cache = @statements[sql] ||= {
+ :stmt => @connection.prepare(sql)
+ }
+ stmt = cache[:stmt]
+ cols = cache[:cols] ||= stmt.columns
+ stmt.reset!
+ stmt.bind_params binds.map { |col, val|
+ col ? col.type_cast(val) : val
+ }
+ end
+
+ ActiveRecord::Result.new(cols, stmt.to_a)
+ end
+ end
+
def execute(sql, name = nil) #:nodoc:
log(sql, name) { @connection.execute(sql) }
end
@@ -151,9 +193,19 @@ module ActiveRecord
alias :create :insert_sql
def select_rows(sql, name = nil)
- execute(sql, name).map do |row|
- (0...(row.size / 2)).map { |i| row[i] }
- end
+ exec_query(sql, name).rows
+ end
+
+ def create_savepoint
+ execute("SAVEPOINT #{current_savepoint_name}")
+ end
+
+ def rollback_to_savepoint
+ execute("ROLLBACK TO SAVEPOINT #{current_savepoint_name}")
+ end
+
+ def release_savepoint
+ execute("RELEASE SAVEPOINT #{current_savepoint_name}")
end
def begin_db_transaction #:nodoc:
@@ -177,24 +229,33 @@ module ActiveRecord
WHERE type = 'table' AND NOT name = 'sqlite_sequence'
SQL
- execute(sql, name).map do |row|
+ exec_query(sql, name).map do |row|
row['name']
end
end
def columns(table_name, name = nil) #:nodoc:
table_structure(table_name).map do |field|
+ case field["dflt_value"]
+ when /^null$/i
+ field["dflt_value"] = nil
+ when /^'(.*)'$/
+ field["dflt_value"] = $1.gsub(/''/, "'")
+ when /^"(.*)"$/
+ field["dflt_value"] = $1.gsub(/""/, '"')
+ end
+
SQLiteColumn.new(field['name'], field['dflt_value'], field['type'], field['notnull'].to_i == 0)
end
end
def indexes(table_name, name = nil) #:nodoc:
- execute("PRAGMA index_list(#{quote_table_name(table_name)})", name).map do |row|
+ exec_query("PRAGMA index_list(#{quote_table_name(table_name)})", name).map do |row|
IndexDefinition.new(
table_name,
row['name'],
- row['unique'].to_i != 0,
- execute("PRAGMA index_info('#{row['name']}')").map { |col|
+ row['unique'] != 0,
+ exec_query("PRAGMA index_info('#{row['name']}')").map { |col|
col['name']
})
end
@@ -202,17 +263,17 @@ module ActiveRecord
def primary_key(table_name) #:nodoc:
column = table_structure(table_name).find { |field|
- field['pk'].to_i == 1
+ field['pk'] == 1
}
column && column['name']
end
def remove_index!(table_name, index_name) #:nodoc:
- execute "DROP INDEX #{quote_column_name(index_name)}"
+ exec_query "DROP INDEX #{quote_column_name(index_name)}"
end
def rename_table(name, new_name)
- execute "ALTER TABLE #{quote_table_name(name)} RENAME TO #{quote_table_name(new_name)}"
+ exec_query "ALTER TABLE #{quote_table_name(name)} RENAME TO #{quote_table_name(new_name)}"
end
# See: http://www.sqlite.org/lang_altertable.html
@@ -249,7 +310,7 @@ module ActiveRecord
def change_column_null(table_name, column_name, null, default = nil)
unless null || default.nil?
- execute("UPDATE #{quote_table_name(table_name)} SET #{quote_column_name(column_name)}=#{quote(default)} WHERE #{quote_column_name(column_name)} IS NULL")
+ exec_query("UPDATE #{quote_table_name(table_name)} SET #{quote_column_name(column_name)}=#{quote(default)} WHERE #{quote_column_name(column_name)} IS NULL")
end
alter_table(table_name) do |definition|
definition[column_name].null = null
@@ -275,23 +336,21 @@ module ActiveRecord
alter_table(table_name, :rename => {column_name.to_s => new_column_name.to_s})
end
+ def null_insert_value
+ Arel.sql 'NULL'
+ end
+
def empty_insert_statement_value
"VALUES(NULL)"
end
protected
- def select(sql, name = nil) #:nodoc:
- execute(sql, name).map do |row|
- record = {}
- row.each do |key, value|
- record[key.sub(/^"?\w+"?\./, '')] = value if key.is_a?(String)
- end
- record
- end
+ def select(sql, name = nil, binds = []) #:nodoc:
+ exec_query(sql, name, binds).to_a
end
def table_structure(table_name)
- structure = @connection.table_info(quote_table_name(table_name))
+ structure = exec_query("PRAGMA table_info(#{quote_table_name(table_name)})").to_hash
raise(ActiveRecord::StatementInvalid, "Could not find table '#{table_name}'") if structure.empty?
structure
end
@@ -345,7 +404,7 @@ module ActiveRecord
name = name[5..-1]
end
- to_column_names = columns(to).map(&:name)
+ to_column_names = columns(to).map { |c| c.name }
columns = index.columns.map {|c| rename[c] || c }.select do |column|
to_column_names.include?(column)
end
@@ -360,18 +419,18 @@ module ActiveRecord
end
def copy_table_contents(from, to, columns, rename = {}) #:nodoc:
- column_mappings = Hash[*columns.map {|name| [name, name]}.flatten]
- rename.inject(column_mappings) {|map, a| map[a.last] = a.first; map}
+ column_mappings = Hash[columns.map {|name| [name, name]}]
+ rename.each { |a| column_mappings[a.last] = a.first }
from_columns = columns(from).collect {|col| col.name}
columns = columns.find_all{|col| from_columns.include?(column_mappings[col])}
quoted_columns = columns.map { |col| quote_column_name(col) } * ','
quoted_to = quote_table_name(to)
- @connection.execute "SELECT * FROM #{quote_table_name(from)}" do |row|
+ exec_query("SELECT * FROM #{quote_table_name(from)}").each do |row|
sql = "INSERT INTO #{quoted_to} (#{quoted_columns}) VALUES ("
sql << columns.map {|col| quote row[column_mappings[col]]} * ', '
sql << ')'
- @connection.execute sql
+ exec_query sql
end
end
diff --git a/activerecord/lib/active_record/counter_cache.rb b/activerecord/lib/active_record/counter_cache.rb
index 237cd56683..7839f03848 100644
--- a/activerecord/lib/active_record/counter_cache.rb
+++ b/activerecord/lib/active_record/counter_cache.rb
@@ -30,9 +30,10 @@ module ActiveRecord
reflection = belongs_to.find { |e| e.class_name == expected_name }
counter_name = reflection.counter_cache_column
- self.unscoped.where(arel_table[self.primary_key].eq(object.id)).arel.update({
+ stmt = unscoped.where(arel_table[primary_key].eq(object.id)).arel.compile_update({
arel_table[counter_name] => object.send(association).count
})
+ connection.update stmt.to_sql
end
return true
end
@@ -56,15 +57,15 @@ module ActiveRecord
# Post.update_counters 5, :comment_count => -1, :action_count => 1
# # Executes the following SQL:
# # UPDATE posts
- # # SET comment_count = comment_count - 1,
- # # action_count = action_count + 1
+ # # SET comment_count = COALESCE(comment_count, 0) - 1,
+ # # action_count = COALESCE(action_count, 0) + 1
# # WHERE id = 5
#
# # For the Posts with id of 10 and 15, increment the comment_count by 1
# Post.update_counters [10, 15], :comment_count => 1
# # Executes the following SQL:
# # UPDATE posts
- # # SET comment_count = comment_count + 1,
+ # # SET comment_count = COALESCE(comment_count, 0) + 1,
# # WHERE id IN (10, 15)
def update_counters(id, counters)
updates = counters.map do |counter_name, value|
@@ -73,6 +74,8 @@ module ActiveRecord
"#{quoted_column} = COALESCE(#{quoted_column}, 0) #{operator} #{value.abs}"
end
+ IdentityMap.remove_by_id(symbolized_base_class, id) if IdentityMap.enabled?
+
update_all(updates.join(', '), primary_key => id )
end
diff --git a/activerecord/lib/active_record/dynamic_finder_match.rb b/activerecord/lib/active_record/dynamic_finder_match.rb
index 0dc965bd26..b309df9b1b 100644
--- a/activerecord/lib/active_record/dynamic_finder_match.rb
+++ b/activerecord/lib/active_record/dynamic_finder_match.rb
@@ -1,45 +1,48 @@
module ActiveRecord
# = Active Record Dynamic Finder Match
- #
+ #
# Refer to ActiveRecord::Base documentation for Dynamic attribute-based finders for detailed info
#
class DynamicFinderMatch
def self.match(method)
- df_match = self.new(method)
- df_match.finder ? df_match : nil
- end
-
- def initialize(method)
- @finder = :first
- @bang = false
- @instantiator = nil
+ finder = :first
+ bang = false
+ instantiator = nil
case method.to_s
- when /^find_(all_by|last_by|by)_([_a-zA-Z]\w*)$/
- @finder = :last if $1 == 'last_by'
- @finder = :all if $1 == 'all_by'
+ when /^find_(all_|last_)?by_([_a-zA-Z]\w*)$/
+ finder = :last if $1 == 'last_'
+ finder = :all if $1 == 'all_'
names = $2
when /^find_by_([_a-zA-Z]\w*)\!$/
- @bang = true
+ bang = true
names = $1
when /^find_or_(initialize|create)_by_([_a-zA-Z]\w*)$/
- @instantiator = $1 == 'initialize' ? :new : :create
+ instantiator = $1 == 'initialize' ? :new : :create
names = $2
else
- @finder = nil
+ return nil
end
- @attribute_names = names && names.split('_and_')
+
+ new(finder, instantiator, bang, names.split('_and_'))
+ end
+
+ def initialize(finder, instantiator, bang, attribute_names)
+ @finder = finder
+ @instantiator = instantiator
+ @bang = bang
+ @attribute_names = attribute_names
end
attr_reader :finder, :attribute_names, :instantiator
def finder?
- !@finder.nil? && @instantiator.nil?
+ @finder && !@instantiator
end
def instantiator?
- @finder == :first && !@instantiator.nil?
+ @finder == :first && @instantiator
end
def creator?
diff --git a/activerecord/lib/active_record/dynamic_scope_match.rb b/activerecord/lib/active_record/dynamic_scope_match.rb
index 15f65be6bc..c832e927d6 100644
--- a/activerecord/lib/active_record/dynamic_scope_match.rb
+++ b/activerecord/lib/active_record/dynamic_scope_match.rb
@@ -1,32 +1,23 @@
module ActiveRecord
# = Active Record Dynamic Scope Match
- #
+ #
# Provides dynamic attribute-based scopes such as <tt>scoped_by_price(4.99)</tt>
# if, for example, the <tt>Product</tt> has an attribute with that name. You can
# chain more <tt>scoped_by_* </tt> methods after the other. It acts like a named
# scope except that it's dynamic.
class DynamicScopeMatch
def self.match(method)
- ds_match = self.new(method)
- ds_match.scope ? ds_match : nil
+ return unless method.to_s =~ /^scoped_by_([_a-zA-Z]\w*)$/
+ new(true, $1 && $1.split('_and_'))
end
- def initialize(method)
- @scope = true
- case method.to_s
- when /^scoped_by_([_a-zA-Z]\w*)$/
- names = $1
- else
- @scope = nil
- end
- @attribute_names = names && names.split('_and_')
+ def initialize(scope, attribute_names)
+ @scope = scope
+ @attribute_names = attribute_names
end
attr_reader :scope, :attribute_names
-
- def scope?
- !@scope.nil?
- end
+ alias :scope? :scope
end
end
diff --git a/activerecord/lib/active_record/errors.rb b/activerecord/lib/active_record/errors.rb
index e9ac5516ec..ea1709cb1f 100644
--- a/activerecord/lib/active_record/errors.rb
+++ b/activerecord/lib/active_record/errors.rb
@@ -30,7 +30,7 @@ module ActiveRecord
class SerializationTypeMismatch < ActiveRecordError
end
- # Raised when adapter not specified on connection (or configuration file <tt>config/database.yml</tt>
+ # Raised when adapter not specified on connection (or configuration file <tt>config/database.yml</tt>
# misses adapter field).
class AdapterNotSpecified < ActiveRecordError
end
@@ -39,7 +39,7 @@ module ActiveRecord
class AdapterNotFound < ActiveRecordError
end
- # Raised when connection to the database could not been established (for example when <tt>connection=</tt>
+ # Raised when connection to the database could not been established (for example when <tt>connection=</tt>
# is given a nil object).
class ConnectionNotEstablished < ActiveRecordError
end
@@ -53,7 +53,7 @@ module ActiveRecord
class RecordNotSaved < ActiveRecordError
end
- # Raised when SQL statement cannot be executed by the database (for example, it's often the case for
+ # Raised when SQL statement cannot be executed by the database (for example, it's often the case for
# MySQL when Ruby driver used is too old).
class StatementInvalid < ActiveRecordError
end
@@ -81,7 +81,7 @@ module ActiveRecord
class InvalidForeignKey < WrappedDatabaseException
end
- # Raised when number of bind variables in statement given to <tt>:condition</tt> key (for example,
+ # Raised when number of bind variables in statement given to <tt>:condition</tt> key (for example,
# when using +find+ method)
# does not match number of expected variables.
#
diff --git a/activerecord/lib/active_record/fixtures.rb b/activerecord/lib/active_record/fixtures.rb
index e44102b538..d523c643ba 100644
--- a/activerecord/lib/active_record/fixtures.rb
+++ b/activerecord/lib/active_record/fixtures.rb
@@ -1,4 +1,10 @@
require 'erb'
+
+begin
+ require 'psych'
+rescue LoadError
+end
+
require 'yaml'
require 'csv'
require 'zlib'
@@ -6,15 +12,7 @@ require 'active_support/dependencies'
require 'active_support/core_ext/array/wrap'
require 'active_support/core_ext/object/blank'
require 'active_support/core_ext/logger'
-
-if RUBY_VERSION < '1.9'
- module YAML #:nodoc:
- class Omap #:nodoc:
- def keys; map { |k, v| k } end
- def values; map { |k, v| v } end
- end
- end
-end
+require 'active_support/ordered_hash'
if defined? ActiveRecord
class FixtureClassNotFound < ActiveRecord::ActiveRecordError #:nodoc:
@@ -24,6 +22,8 @@ else
end
end
+class FixturesFileNotFound < StandardError; end
+
# Fixtures are a way of organizing data that you want to test against; in short, sample data.
#
# = Fixture formats
@@ -39,9 +39,9 @@ end
# 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+.
#
-# Unlike single-file fixtures, YAML fixtures are stored in a single file per model, which are placed
-# in the directory appointed by <tt>ActiveSupport::TestCase.fixture_path=(path)</tt> (this is
-# automatically configured for Rails, so you can just put your files in <tt><your-rails-app>/test/fixtures/</tt>).
+# Unlike single-file fixtures, YAML fixtures are stored in a single file per model, which are placed
+# in the directory appointed by <tt>ActiveSupport::TestCase.fixture_path=(path)</tt> (this is
+# automatically configured for Rails, so you can just put your files in <tt><your-rails-app>/test/fixtures/</tt>).
# The fixture file ends with the <tt>.yml</tt> file extension (Rails example:
# <tt><your-rails-app>/test/fixtures/web_sites.yml</tt>). The format of a YAML fixture file looks like this:
#
@@ -59,7 +59,7 @@ end
# indented list of key/value pairs in the "key: value" format. Records are separated by a blank line for your viewing
# pleasure.
#
-# Note that YAML fixtures are unordered. If you want ordered fixtures, use the omap YAML type.
+# Note that YAML fixtures are unordered. If you want ordered fixtures, use the omap YAML type.
# See http://yaml.org/type/omap.html
# for the specification. You will need ordered fixtures when you have foreign key constraints on keys in the same table.
# This is commonly needed for tree structures. Example:
@@ -81,7 +81,7 @@ end
# (Rails example: <tt><your-rails-app>/test/fixtures/web_sites.csv</tt>).
#
# The format of this type of fixture file is much more compact than the others, but also a little harder to read by us
-# humans. The first line of the CSV file is a comma-separated list of field names. The rest of the
+# humans. The first line of the CSV file is a comma-separated list of field names. The rest of the
# file is then comprised
# of the actual data (1 per line). Here's an example:
#
@@ -102,15 +102,15 @@ end
#
# == Single-file fixtures
#
-# This type of fixture was the original format for Active Record that has since been deprecated in
+# 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 appointed by <tt>ActiveSupport::TestCase.fixture_path=(path)</tt> (this is automatically
+# Fixtures for this format are created by placing text files in a sub-directory (with the name of the model)
+# to the directory appointed by <tt>ActiveSupport::TestCase.fixture_path=(path)</tt> (this is automatically
# configured for Rails, so you can just put your files in <tt><your-rails-app>/test/fixtures/<your-model-name>/</tt> --
# like <tt><your-rails-app>/test/fixtures/web_sites/</tt> for the WebSite model).
#
# Each text file placed in this directory represents a "record". Usually these types of fixtures are named without
-# extensions, but if you are on a Windows machine, you might consider adding <tt>.txt</tt> as the extension.
+# extensions, but if you are on a Windows machine, you might consider adding <tt>.txt</tt> as the extension.
# Here's what the above example might look like:
#
# web_sites/google
@@ -137,7 +137,7 @@ end
# end
# end
#
-# By default, the <tt>test_helper module</tt> will load all of your fixtures into your test database,
+# 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.
@@ -187,14 +187,14 @@ end
# This will create 1000 very simple YAML fixtures.
#
# Using ERb, you can also inject dynamic values into your fixtures with inserts like <tt><%= Date.today.strftime("%Y-%m-%d") %></tt>.
-# This is however a feature to be used with some caution. The point of fixtures are that they're
-# stable units of predictable sample data. If you feel that you need to inject dynamic values, then
-# perhaps you should reexamine whether your application is properly testable. Hence, dynamic values
+# This is however a feature to be used with some caution. The point of fixtures are that they're
+# stable units of predictable sample data. If you feel that you need to inject dynamic values, then
+# perhaps you should reexamine whether your application is properly testable. Hence, dynamic values
# in fixtures are to be considered a code smell.
#
# = Transactional fixtures
#
-# TestCases can use begin+rollback to isolate their changes to the database instead of having to
+# TestCases can use begin+rollback to isolate their changes to the database instead of having to
# delete+insert for every test case.
#
# class FooTest < ActiveSupport::TestCase
@@ -212,17 +212,17 @@ end
# end
#
# If you preload your test database with all fixture data (probably in the Rakefile task) and use transactional fixtures,
-# then you may omit all fixtures declarations in your test cases since all the data's already there
+# then you may omit all fixtures declarations in your test cases since all the data's already there
# and every case rolls back its changes.
#
# In order to use instantiated fixtures with preloaded data, set +self.pre_loaded_fixtures+ to true. This will provide
-# access to fixture data for every table that has been loaded through fixtures (depending on the
+# 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
+# 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.
@@ -444,20 +444,23 @@ end
#
# Any fixture labeled "DEFAULTS" is safely ignored.
-class Fixtures < (RUBY_VERSION < '1.9' ? YAML::Omap : Hash)
+class Fixtures
MAX_ID = 2 ** 30 - 1
- DEFAULT_FILTER_RE = /\.ya?ml$/
- @@all_cached_fixtures = {}
+ @@all_cached_fixtures = Hash.new { |h,k| h[k] = {} }
- def self.reset_cache(connection = nil)
- connection ||= ActiveRecord::Base.connection
- @@all_cached_fixtures[connection.object_id] = {}
+ def self.find_table_name(table_name) # :nodoc:
+ ActiveRecord::Base.pluralize_table_names ?
+ table_name.to_s.singularize.camelize :
+ table_name.to_s.camelize
+ end
+
+ def self.reset_cache
+ @@all_cached_fixtures.clear
end
def self.cache_for_connection(connection)
- @@all_cached_fixtures[connection.object_id] ||= {}
- @@all_cached_fixtures[connection.object_id]
+ @@all_cached_fixtures[connection]
end
def self.fixture_is_cached?(connection, table_name)
@@ -466,27 +469,23 @@ class Fixtures < (RUBY_VERSION < '1.9' ? YAML::Omap : Hash)
def self.cached_fixtures(connection, keys_to_fetch = nil)
if keys_to_fetch
- fixtures = cache_for_connection(connection).values_at(*keys_to_fetch)
+ cache_for_connection(connection).values_at(*keys_to_fetch)
else
- fixtures = cache_for_connection(connection).values
+ cache_for_connection(connection).values
end
- fixtures.size > 1 ? fixtures : fixtures.first
end
def self.cache_fixtures(connection, fixtures_map)
cache_for_connection(connection).update(fixtures_map)
end
- def self.instantiate_fixtures(object, table_name, fixtures, load_instances = true)
- object.instance_variable_set "@#{table_name.to_s.gsub('.','_')}", fixtures
+ def self.instantiate_fixtures(object, fixture_name, fixtures, load_instances = true)
if load_instances
- ActiveRecord::Base.silence do
- fixtures.each do |name, fixture|
- begin
- object.instance_variable_set "@#{name}", fixture.find
- rescue FixtureClassNotFound
- nil
- end
+ fixtures.each do |name, fixture|
+ begin
+ object.instance_variable_set "@#{name}", fixture.find
+ rescue FixtureClassNotFound
+ nil
end
end
end
@@ -503,36 +502,56 @@ class Fixtures < (RUBY_VERSION < '1.9' ? YAML::Omap : Hash)
def self.create_fixtures(fixtures_directory, table_names, class_names = {})
table_names = [table_names].flatten.map { |n| n.to_s }
- table_names.each { |n| class_names[n.tr('/', '_').to_sym] = n.classify if n.include?('/') }
- connection = block_given? ? yield : ActiveRecord::Base.connection
+ table_names.each { |n|
+ class_names[n.tr('/', '_').to_sym] = n.classify if n.include?('/')
+ }
- table_names_to_fetch = table_names.reject { |table_name| fixture_is_cached?(connection, table_name) }
+ # FIXME: Apparently JK uses this.
+ connection = block_given? ? yield : ActiveRecord::Base.connection
- unless table_names_to_fetch.empty?
- ActiveRecord::Base.silence do
- connection.disable_referential_integrity do
- fixtures_map = {}
+ files_to_read = table_names.reject { |table_name| fixture_is_cached?(connection, table_name) }
- fixtures = table_names_to_fetch.map do |table_name|
- fixtures_map[table_name] = Fixtures.new(connection, table_name.tr('/', '_'), class_names[table_name.tr('/', '_').to_sym], File.join(fixtures_directory, table_name))
- end
+ unless files_to_read.empty?
+ connection.disable_referential_integrity do
+ fixtures_map = {}
+
+ fixture_files = files_to_read.map do |path|
+ table_name = path.tr '/', '_'
+
+ fixtures_map[path] = Fixtures.new(
+ connection,
+ table_name,
+ class_names[table_name.to_sym],
+ File.join(fixtures_directory, path))
+ end
+
+ all_loaded_fixtures.update(fixtures_map)
- all_loaded_fixtures.update(fixtures_map)
+ connection.transaction(:requires_new => true) do
+ fixture_files.each do |ff|
+ conn = ff.model_class.respond_to?(:connection) ? ff.model_class.connection : connection
+ table_rows = ff.table_rows
- connection.transaction(:requires_new => true) do
- fixtures.reverse.each { |fixture| fixture.delete_existing_fixtures }
- fixtures.each { |fixture| fixture.insert_fixtures }
+ table_rows.keys.each do |table|
+ conn.delete "DELETE FROM #{conn.quote_table_name(table)}", 'Fixture Delete'
+ end
- # Cap primary key sequences to max(pk).
- if connection.respond_to?(:reset_pk_sequence!)
- table_names.each do |table_name|
- connection.reset_pk_sequence!(table_name.tr('/', '_'))
+ table_rows.each do |table_name,rows|
+ rows.each do |row|
+ conn.insert_fixture(row, table_name)
end
end
end
- cache_fixtures(connection, fixtures_map)
+ # Cap primary key sequences to max(pk).
+ if connection.respond_to?(:reset_pk_sequence!)
+ table_names.each do |table_name|
+ connection.reset_pk_sequence!(table_name.tr('/', '_'))
+ end
+ end
end
+
+ cache_fixtures(connection, fixtures_map)
end
end
cached_fixtures(connection, table_names)
@@ -544,40 +563,59 @@ class Fixtures < (RUBY_VERSION < '1.9' ? YAML::Omap : Hash)
Zlib.crc32(label.to_s) % MAX_ID
end
- attr_reader :table_name, :name
+ attr_reader :table_name, :name, :fixtures, :model_class
+
+ def initialize(connection, table_name, class_name, fixture_path)
+ @connection = connection
+ @table_name = table_name
+ @fixture_path = fixture_path
+ @name = table_name # preserve fixture base name
+ @class_name = class_name
+
+ @fixtures = ActiveSupport::OrderedHash.new
+ @table_name = "#{ActiveRecord::Base.table_name_prefix}#{@table_name}#{ActiveRecord::Base.table_name_suffix}"
+
+ # Should be an AR::Base type class
+ if class_name.is_a?(Class)
+ @table_name = class_name.table_name
+ @connection = class_name.connection
+ @model_class = class_name
+ else
+ @model_class = class_name.constantize rescue nil
+ end
- def initialize(connection, table_name, class_name, fixture_path, file_filter = DEFAULT_FILTER_RE)
- @connection, @table_name, @fixture_path, @file_filter = connection, table_name, fixture_path, file_filter
- @name = table_name # preserve fixture base name
- @class_name = class_name ||
- (ActiveRecord::Base.pluralize_table_names ? @table_name.singularize.camelize : @table_name.camelize)
- @table_name = "#{ActiveRecord::Base.table_name_prefix}#{@table_name}#{ActiveRecord::Base.table_name_suffix}"
- @table_name = class_name.table_name if class_name.respond_to?(:table_name)
- @connection = class_name.connection if class_name.respond_to?(:connection)
read_fixture_files
end
- def delete_existing_fixtures
- @connection.delete "DELETE FROM #{@connection.quote_table_name(table_name)}", 'Fixture Delete'
+ def [](x)
+ fixtures[x]
+ end
+
+ def []=(k,v)
+ fixtures[k] = v
+ end
+
+ def each(&block)
+ fixtures.each(&block)
+ end
+
+ def size
+ fixtures.size
end
- def insert_fixtures
+ # Return a hash of rows to be inserted. The key is the table, the value is
+ # a list of rows to insert to that table.
+ def table_rows
now = ActiveRecord::Base.default_timezone == :utc ? Time.now.utc : Time.now
now = now.to_s(:db)
# allow a standard key to be used for doing defaults in YAML
- if is_a?(Hash)
- delete('DEFAULTS')
- else
- delete(assoc('DEFAULTS'))
- end
+ fixtures.delete('DEFAULTS')
# track any join tables we need to insert later
- habtm_fixtures = Hash.new do |h, habtm|
- h[habtm] = HabtmFixtures.new(@connection, habtm.options[:join_table], nil, nil)
- end
+ rows = Hash.new { |h,table| h[table] = [] }
- each do |label, fixture|
+ rows[table_name] = fixtures.map do |label, fixture|
row = fixture.to_hash
if model_class && model_class < ActiveRecord::Base
@@ -613,14 +651,9 @@ class Fixtures < (RUBY_VERSION < '1.9' ? YAML::Omap : Hash)
fk_name = (association.options[:foreign_key] || "#{association.name}_id").to_s
if association.name.to_s != fk_name && value = row.delete(association.name.to_s)
- if association.options[:polymorphic]
- if value.sub!(/\s*\(([^\)]*)\)\s*$/, "")
- target_type = $1
- target_type_name = (association.options[:foreign_type] || "#{association.name}_type").to_s
-
- # support polymorphic belongs_to as "label (Type)"
- row[target_type_name] = target_type
- end
+ if association.options[:polymorphic] && value.sub!(/\s*\(([^\)]*)\)\s*$/, "")
+ # support polymorphic belongs_to as "label (Type)"
+ row[association.foreign_type] = $1
end
row[fk_name] = Fixtures.identify(value)
@@ -628,47 +661,22 @@ class Fixtures < (RUBY_VERSION < '1.9' ? YAML::Omap : Hash)
when :has_and_belongs_to_many
if (targets = row.delete(association.name.to_s))
targets = targets.is_a?(Array) ? targets : targets.split(/\s*,\s*/)
- join_fixtures = habtm_fixtures[association]
-
- targets.each do |target|
- join_fixtures["#{label}_#{target}"] = Fixture.new(
- { association.primary_key_name => row[primary_key_name],
- association.association_foreign_key => Fixtures.identify(target) },
- nil, @connection)
- end
+ table_name = association.options[:join_table]
+ rows[table_name].concat targets.map { |target|
+ { association.foreign_key => row[primary_key_name],
+ association.association_foreign_key => Fixtures.identify(target) }
+ }
end
end
end
end
- @connection.insert_fixture(fixture, @table_name)
- end
-
- # insert any HABTM join tables we discovered
- habtm_fixtures.values.each do |fixture|
- fixture.delete_existing_fixtures
- fixture.insert_fixtures
+ row
end
+ rows
end
private
- class HabtmFixtures < ::Fixtures #:nodoc:
- def read_fixture_files; end
- end
-
- def model_class
- unless defined?(@model_class)
- @model_class =
- if @class_name.nil? || @class_name.is_a?(Class)
- @class_name
- else
- @class_name.constantize rescue nil
- end
- end
-
- @model_class
- end
-
def primary_key_name
@primary_key_name ||= model_class && model_class.primary_key
end
@@ -688,7 +696,7 @@ class Fixtures < (RUBY_VERSION < '1.9' ? YAML::Omap : Hash)
end
def column_names
- @column_names ||= @connection.columns(@table_name).collect(&:name)
+ @column_names ||= @connection.columns(@table_name).collect { |c| c.name }
end
def read_fixture_files
@@ -696,15 +704,15 @@ class Fixtures < (RUBY_VERSION < '1.9' ? YAML::Omap : Hash)
read_yaml_fixture_files
elsif File.file?(csv_file_path)
read_csv_fixture_files
+ else
+ raise FixturesFileNotFound, "Could not find #{yaml_file_path} or #{csv_file_path}"
end
end
def read_yaml_fixture_files
- yaml_string = ""
- Dir["#{@fixture_path}/**/*.yml"].select { |f| test(?f, f) }.each do |subfixture_path|
- yaml_string << IO.read(subfixture_path)
- end
- yaml_string << IO.read(yaml_file_path)
+ yaml_string = (Dir["#{@fixture_path}/**/*.yml"].select { |f|
+ File.file?(f)
+ } + [yaml_file_path]).map { |file_path| IO.read(file_path) }.join
if yaml = parse_yaml_string(yaml_string)
# If the file is an ordered map, extract its children.
@@ -722,7 +730,7 @@ class Fixtures < (RUBY_VERSION < '1.9' ? YAML::Omap : Hash)
raise Fixture::FormatError, "Bad data for #{@class_name} fixture named #{name} (nil)"
end
- self[name] = Fixture.new(data, model_class, @connection)
+ fixtures[name] = Fixture.new(data, model_class)
end
end
end
@@ -735,7 +743,7 @@ class Fixtures < (RUBY_VERSION < '1.9' ? YAML::Omap : Hash)
reader.each do |row|
data = {}
row.each_with_index { |cell, j| data[header[j].to_s.strip] = cell.to_s.strip }
- self["#{@class_name.to_s.underscore}_#{i+=1}"] = Fixture.new(data, model_class, @connection)
+ fixtures["#{@class_name.to_s.underscore}_#{i+=1}"] = Fixture.new(data, model_class)
end
end
@@ -771,44 +779,30 @@ class Fixture #:nodoc:
class FormatError < FixtureError #:nodoc:
end
- attr_reader :model_class
+ attr_reader :model_class, :fixture
- def initialize(fixture, model_class, connection = ActiveRecord::Base.connection)
- @connection = connection
- @fixture = fixture
- @model_class = model_class.is_a?(Class) ? model_class : model_class.constantize rescue nil
+ def initialize(fixture, model_class)
+ @fixture = fixture
+ @model_class = model_class
end
def class_name
- @model_class.name if @model_class
+ model_class.name if model_class
end
def each
- @fixture.each { |item| yield item }
+ fixture.each { |item| yield item }
end
def [](key)
- @fixture[key]
- end
-
- def to_hash
- @fixture
+ fixture[key]
end
- def key_list
- @fixture.keys.map { |column_name| @connection.quote_column_name(column_name) }.join(', ')
- end
-
- def value_list
- cols = (model_class && model_class < ActiveRecord::Base) ? model_class.columns_hash : {}
- @fixture.map do |key, value|
- @connection.quote(value, cols[key]).gsub('[^\]\\n', "\n").gsub('[^\]\\r', "\r")
- end.join(', ')
- end
+ alias :to_hash :fixture
def find
if model_class
- model_class.find(self[model_class.primary_key])
+ model_class.find(fixture[model_class.primary_key])
else
raise FixtureClassNotFound, "No class attached to find."
end
@@ -823,19 +817,21 @@ module ActiveRecord
setup :setup_fixtures
teardown :teardown_fixtures
- superclass_delegating_accessor :fixture_path
- superclass_delegating_accessor :fixture_table_names
- superclass_delegating_accessor :fixture_class_names
- superclass_delegating_accessor :use_transactional_fixtures
- superclass_delegating_accessor :use_instantiated_fixtures # true, false, or :no_instances
- superclass_delegating_accessor :pre_loaded_fixtures
+ class_attribute :fixture_path
+ class_attribute :fixture_table_names
+ class_attribute :fixture_class_names
+ class_attribute :use_transactional_fixtures
+ class_attribute :use_instantiated_fixtures # true, false, or :no_instances
+ class_attribute :pre_loaded_fixtures
self.fixture_table_names = []
self.use_transactional_fixtures = true
self.use_instantiated_fixtures = false
self.pre_loaded_fixtures = false
- self.fixture_class_names = {}
+ self.fixture_class_names = Hash.new do |h, table_name|
+ h[table_name] = Fixtures.find_table_name(table_name)
+ end
end
module ClassMethods
@@ -843,17 +839,17 @@ module ActiveRecord
self.fixture_class_names = self.fixture_class_names.merge(class_names)
end
- def fixtures(*table_names)
- if table_names.first == :all
- table_names = Dir["#{fixture_path}/**/*.{yml,csv}"]
- table_names.map! { |f| f[(fixture_path.size + 1)..-5] }
+ def fixtures(*fixture_names)
+ if fixture_names.first == :all
+ fixture_names = Dir["#{fixture_path}/**/*.{yml,csv}"]
+ fixture_names.map! { |f| f[(fixture_path.size + 1)..-5] }
else
- table_names = table_names.flatten.map { |n| n.to_s }
+ fixture_names = fixture_names.flatten.map { |n| n.to_s }
end
- self.fixture_table_names |= table_names
- require_fixture_classes(table_names)
- setup_fixture_accessors(table_names)
+ self.fixture_table_names |= fixture_names
+ require_fixture_classes(fixture_names)
+ setup_fixture_accessors(fixture_names)
end
def try_to_load_dependency(file_name)
@@ -868,43 +864,48 @@ module ActiveRecord
end
end
- def require_fixture_classes(table_names = nil)
- (table_names || fixture_table_names).each do |table_name|
- file_name = table_name.to_s
+ def require_fixture_classes(fixture_names = nil)
+ (fixture_names || fixture_table_names).each do |fixture_name|
+ file_name = fixture_name.to_s
file_name = file_name.singularize if ActiveRecord::Base.pluralize_table_names
try_to_load_dependency(file_name)
end
end
- def setup_fixture_accessors(table_names = nil)
- table_names = Array.wrap(table_names || fixture_table_names)
- table_names.each do |table_name|
- table_name = table_name.to_s.tr('./', '_')
+ def setup_fixture_accessors(fixture_names = nil)
+ fixture_names = Array.wrap(fixture_names || fixture_table_names)
+ methods = Module.new do
+ fixture_names.each do |fixture_name|
+ fixture_name = fixture_name.to_s.tr('./', '_')
- redefine_method(table_name) do |*fixtures|
- force_reload = fixtures.pop if fixtures.last == true || fixtures.last == :reload
+ define_method(fixture_name) do |*fixtures|
+ force_reload = fixtures.pop if fixtures.last == true || fixtures.last == :reload
- @fixture_cache[table_name] ||= {}
+ @fixture_cache[fixture_name] ||= {}
- instances = fixtures.map do |fixture|
- @fixture_cache[table_name].delete(fixture) if force_reload
+ instances = fixtures.map do |fixture|
+ @fixture_cache[fixture_name].delete(fixture) if force_reload
- if @loaded_fixtures[table_name][fixture.to_s]
- @fixture_cache[table_name][fixture] ||= @loaded_fixtures[table_name][fixture.to_s].find
- else
- raise StandardError, "No fixture with name '#{fixture}' found for table '#{table_name}'"
+ if @loaded_fixtures[fixture_name][fixture.to_s]
+ ActiveRecord::IdentityMap.without do
+ @fixture_cache[fixture_name][fixture] ||= @loaded_fixtures[fixture_name][fixture.to_s].find
+ end
+ else
+ raise StandardError, "No fixture with name '#{fixture}' found for table '#{fixture_name}'"
+ end
end
- end
- instances.size == 1 ? instances.first : instances
+ instances.size == 1 ? instances.first : instances
+ end
+ private fixture_name
end
- private table_name
end
+ include methods
end
def uses_transaction(*methods)
@uses_transaction = [] unless defined?(@uses_transaction)
- @uses_transaction.concat methods.map(&:to_s)
+ @uses_transaction.concat methods.map { |m| m.to_s }
end
def uses_transaction?(method)
@@ -919,7 +920,7 @@ module ActiveRecord
end
def setup_fixtures
- return unless defined?(ActiveRecord) && !ActiveRecord::Base.configurations.blank?
+ return unless !ActiveRecord::Base.configurations.blank?
if pre_loaded_fixtures && !use_transactional_fixtures
raise RuntimeError, 'pre_loaded_fixtures requires use_transactional_fixtures'
@@ -933,7 +934,7 @@ module ActiveRecord
if @@already_loaded_fixtures[self.class]
@loaded_fixtures = @@already_loaded_fixtures[self.class]
else
- load_fixtures
+ @loaded_fixtures = load_fixtures
@@already_loaded_fixtures[self.class] = @loaded_fixtures
end
ActiveRecord::Base.connection.increment_open_transactions
@@ -943,7 +944,7 @@ module ActiveRecord
else
Fixtures.reset_cache
@@already_loaded_fixtures[self.class] = nil
- load_fixtures
+ @loaded_fixtures = load_fixtures
end
# Instantiate fixtures for every test if requested.
@@ -967,15 +968,8 @@ module ActiveRecord
private
def load_fixtures
- @loaded_fixtures = {}
fixtures = Fixtures.create_fixtures(fixture_path, fixture_table_names, fixture_class_names)
- unless fixtures.nil?
- if fixtures.instance_of?(Fixtures)
- @loaded_fixtures[fixtures.name] = fixtures
- else
- fixtures.each { |f| @loaded_fixtures[f.name] = f }
- end
- end
+ Hash[fixtures.map { |f| [f.name, f] }]
end
# for pre_loaded_fixtures, only require the classes once. huge speed improvement
@@ -991,8 +985,8 @@ module ActiveRecord
Fixtures.instantiate_all_loaded_fixtures(self, load_instances?)
else
raise RuntimeError, 'Load fixtures before instantiating them.' if @loaded_fixtures.nil?
- @loaded_fixtures.each do |table_name, fixtures|
- Fixtures.instantiate_fixtures(self, table_name, fixtures, load_instances?)
+ @loaded_fixtures.each do |fixture_name, fixtures|
+ Fixtures.instantiate_fixtures(self, fixture_name, fixtures, load_instances?)
end
end
end
diff --git a/activerecord/lib/active_record/identity_map.rb b/activerecord/lib/active_record/identity_map.rb
new file mode 100644
index 0000000000..d18b2b0a54
--- /dev/null
+++ b/activerecord/lib/active_record/identity_map.rb
@@ -0,0 +1,102 @@
+module ActiveRecord
+ # = Active Record Identity Map
+ #
+ # Ensures that each object gets loaded only once by keeping every loaded
+ # object in a map. Looks up objects using the map when referring to them.
+ #
+ # More information on Identity Map pattern:
+ # http://www.martinfowler.com/eaaCatalog/identityMap.html
+ #
+ # == Configuration
+ #
+ # In order to enable IdentityMap, set <tt>config.active_record.identity_map = true</tt>
+ # in your <tt>config/application.rb</tt> file.
+ #
+ # IdentityMap is disabled by default.
+ #
+ module IdentityMap
+ extend ActiveSupport::Concern
+
+ class << self
+ def enabled=(flag)
+ Thread.current[:identity_map_enabled] = flag
+ end
+
+ def enabled
+ Thread.current[:identity_map_enabled]
+ end
+ alias enabled? enabled
+
+ def repository
+ Thread.current[:identity_map] ||= Hash.new { |h,k| h[k] = {} }
+ end
+
+ def use
+ old, self.enabled = enabled, true
+
+ yield if block_given?
+ ensure
+ self.enabled = old
+ clear
+ end
+
+ def without
+ old, self.enabled = enabled, false
+
+ yield if block_given?
+ ensure
+ self.enabled = old
+ end
+
+ def get(klass, primary_key)
+ obj = repository[klass.symbolized_base_class][primary_key]
+ obj.is_a?(klass) ? obj : nil
+ end
+
+ def add(record)
+ repository[record.class.symbolized_base_class][record.id] = record
+ end
+
+ def remove(record)
+ repository[record.class.symbolized_base_class].delete(record.id)
+ end
+
+ def remove_by_id(symbolized_base_class, id)
+ repository[symbolized_base_class].delete(id)
+ end
+
+ def clear
+ repository.clear
+ end
+ end
+
+ # Reinitialize an Identity Map model object from +coder+.
+ # +coder+ must contain the attributes necessary for initializing an empty
+ # model object.
+ def reinit_with(coder)
+ @attributes_cache = {}
+ dirty = @changed_attributes.keys
+ @attributes.update(coder['attributes'].except(*dirty))
+ @changed_attributes.update(coder['attributes'].slice(*dirty))
+ @changed_attributes.delete_if{|k,v| v.eql? @attributes[k]}
+
+ set_serialized_attributes
+
+ run_callbacks :find
+
+ self
+ end
+
+ class Middleware
+ def initialize(app)
+ @app = app
+ end
+
+ def call(env)
+ ActiveRecord::IdentityMap.use do
+ @app.call(env)
+ end
+ end
+ end
+ end
+end
diff --git a/activerecord/lib/active_record/locale/en.yml b/activerecord/lib/active_record/locale/en.yml
index a0e94cbec1..44328f63b6 100644
--- a/activerecord/lib/active_record/locale/en.yml
+++ b/activerecord/lib/active_record/locale/en.yml
@@ -22,7 +22,7 @@ en:
# attributes:
# login:
# blank: "This is a custom blank message for User login"
- # Will define custom blank validation message for User model and
+ # Will define custom blank validation message for User model and
# custom blank validation message for login attribute of User model.
#models:
@@ -31,7 +31,7 @@ en:
# For example,
# user: "Dude"
# will translate User model name to "Dude"
-
+
# Translate model attribute names. Used in Model.human_attribute_name(attribute).
#attributes:
# For example,
diff --git a/activerecord/lib/active_record/locking/optimistic.rb b/activerecord/lib/active_record/locking/optimistic.rb
index b6f87a57b8..9a31675782 100644
--- a/activerecord/lib/active_record/locking/optimistic.rb
+++ b/activerecord/lib/active_record/locking/optimistic.rb
@@ -23,7 +23,7 @@ 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:
+ # Optimistic locking will also check for stale data when objects are destroyed. Example:
#
# p1 = Person.find(1)
# p2 = Person.find(1)
@@ -58,6 +58,12 @@ module ActiveRecord
end
private
+ def increment_lock
+ lock_col = self.class.locking_column
+ previous_lock_value = send(lock_col).to_i
+ send(lock_col + '=', previous_lock_value + 1)
+ end
+
def attributes_from_column_definition
result = super
@@ -70,7 +76,7 @@ module ActiveRecord
result[self.class.locking_column] ||= 0
end
- return result
+ result
end
def update(attribute_names = @attributes.keys) #:nodoc:
@@ -78,8 +84,8 @@ module ActiveRecord
return 0 if attribute_names.empty?
lock_col = self.class.locking_column
- previous_value = send(lock_col).to_i
- send(lock_col + '=', previous_value + 1)
+ previous_lock_value = send(lock_col).to_i
+ increment_lock
attribute_names += [lock_col]
attribute_names.uniq!
@@ -87,11 +93,13 @@ module ActiveRecord
begin
relation = self.class.unscoped
- affected_rows = relation.where(
+ stmt = relation.where(
relation.table[self.class.primary_key].eq(quoted_id).and(
- relation.table[self.class.locking_column].eq(quote_value(previous_value))
+ relation.table[lock_col].eq(quote_value(previous_lock_value))
)
- ).arel.update(arel_attributes_values(false, false, attribute_names))
+ ).arel.compile_update(arel_attributes_values(false, false, attribute_names))
+
+ affected_rows = connection.update stmt.to_sql
unless affected_rows == 1
raise ActiveRecord::StaleObjectError, "Attempted to update a stale object: #{self.class.name}"
@@ -101,7 +109,7 @@ module ActiveRecord
# If something went wrong, revert the version.
rescue Exception
- send(lock_col + '=', previous_value)
+ send(lock_col + '=', previous_lock_value)
raise
end
end
@@ -109,13 +117,11 @@ module ActiveRecord
def destroy #:nodoc:
return super unless locking_enabled?
- unless new_record?
- lock_col = self.class.locking_column
- previous_value = send(lock_col).to_i
-
+ if persisted?
table = self.class.arel_table
- predicate = table[self.class.primary_key].eq(id)
- predicate = predicate.and(table[self.class.locking_column].eq(previous_value))
+ lock_col = self.class.locking_column
+ predicate = table[self.class.primary_key].eq(id).
+ and(table[lock_col].eq(send(lock_col).to_i))
affected_rows = self.class.unscoped.where(predicate).delete_all
diff --git a/activerecord/lib/active_record/locking/pessimistic.rb b/activerecord/lib/active_record/locking/pessimistic.rb
index fcc9ebb4af..862cf8f72a 100644
--- a/activerecord/lib/active_record/locking/pessimistic.rb
+++ b/activerecord/lib/active_record/locking/pessimistic.rb
@@ -9,13 +9,12 @@ module ActiveRecord
# Account.find(1, :lock => true)
#
# Pass <tt>:lock => 'some locking clause'</tt> to give a database-specific locking clause
- # of your own such as 'LOCK IN SHARE MODE' or 'FOR UPDATE NOWAIT'.
+ # of your own such as 'LOCK IN SHARE MODE' or 'FOR UPDATE NOWAIT'. Example:
#
- # Example:
# Account.transaction do
# # select * from accounts where name = 'shugo' limit 1 for update
- # shugo = Account.find(:first, :conditions => "name = 'shugo'", :lock => true)
- # yuko = Account.find(:first, :conditions => "name = 'yuko'", :lock => true)
+ # shugo = Account.where("name = 'shugo'").lock(true).first
+ # yuko = Account.where("name = 'shugo'").lock(true).first
# shugo.balance -= 100
# shugo.save!
# yuko.balance += 100
@@ -24,9 +23,10 @@ module ActiveRecord
#
# You can also use ActiveRecord::Base#lock! method to lock one record by id.
# This may be better if you don't need to lock every row. Example:
+ #
# Account.transaction do
# # select * from accounts where ...
- # accounts = Account.find(:all, :conditions => ...)
+ # accounts = Account.where(...).all
# account1 = accounts.detect { |account| ... }
# account2 = accounts.detect { |account| ... }
# # select * from accounts where id=? for update
@@ -40,14 +40,14 @@ module ActiveRecord
#
# Database-specific information on row locking:
# MySQL: http://dev.mysql.com/doc/refman/5.1/en/innodb-locking-reads.html
- # PostgreSQL: http://www.postgresql.org/docs/8.1/interactive/sql-select.html#SQL-FOR-UPDATE-SHARE
+ # PostgreSQL: http://www.postgresql.org/docs/current/interactive/sql-select.html#SQL-FOR-UPDATE-SHARE
module Pessimistic
# Obtain a row lock on this record. Reloads the record to obtain the requested
# lock. Pass an SQL locking clause to append the end of the SELECT statement
- # or pass true for "FOR UPDATE" (the default, an exclusive row lock). Returns
+ # or pass true for "FOR UPDATE" (the default, an exclusive row lock). Returns
# the locked record.
def lock!(lock = true)
- reload(:lock => lock) unless new_record?
+ reload(:lock => lock) if persisted?
self
end
end
diff --git a/activerecord/lib/active_record/log_subscriber.rb b/activerecord/lib/active_record/log_subscriber.rb
index c7ae12977a..afadbf03ef 100644
--- a/activerecord/lib/active_record/log_subscriber.rb
+++ b/activerecord/lib/active_record/log_subscriber.rb
@@ -22,8 +22,16 @@ module ActiveRecord
self.class.runtime += event.duration
return unless logger.debug?
- name = '%s (%.1fms)' % [event.payload[:name], event.duration]
- sql = event.payload[:sql].squeeze(' ')
+ payload = event.payload
+ name = '%s (%.1fms)' % [payload[:name], event.duration]
+ sql = payload[:sql].squeeze(' ')
+ binds = nil
+
+ unless (payload[:binds] || []).empty?
+ binds = " " + payload[:binds].map { |col,v|
+ [col.name, v]
+ }.inspect
+ end
if odd?
name = color(name, CYAN, true)
@@ -32,7 +40,7 @@ module ActiveRecord
name = color(name, MAGENTA, true)
end
- debug " #{name} #{sql}"
+ debug " #{name} #{sql}#{binds}"
end
def odd?
@@ -45,4 +53,4 @@ module ActiveRecord
end
end
-ActiveRecord::LogSubscriber.attach_to :active_record \ No newline at end of file
+ActiveRecord::LogSubscriber.attach_to :active_record
diff --git a/activerecord/lib/active_record/migration.rb b/activerecord/lib/active_record/migration.rb
index 5e272f0ba4..640111096d 100644
--- a/activerecord/lib/active_record/migration.rb
+++ b/activerecord/lib/active_record/migration.rb
@@ -1,5 +1,4 @@
-require 'active_support/core_ext/kernel/singleton_class'
-require 'active_support/core_ext/module/aliasing'
+require "active_support/core_ext/array/wrap"
module ActiveRecord
# Exception that can be raised to stop migrations from going backwards.
@@ -31,39 +30,39 @@ module ActiveRecord
end
# = Active Record Migrations
- #
- # Migrations can manage the evolution of a schema used by several physical
+ #
+ # Migrations can manage the evolution of a schema used by several physical
# databases. It's a solution to the common problem of adding a field to make
# a new feature work in your local database, but being unsure of how to
- # push that change to other developers and to the production server. With
+ # push that change to other developers and to the production server. With
# migrations, you can describe the transformations in self-contained classes
- # that can be checked into version control systems and executed against
+ # that can be checked into version control systems and executed against
# another database that might be one, two, or five versions behind.
#
# Example of a simple migration:
#
# class AddSsl < ActiveRecord::Migration
- # def self.up
+ # def up
# add_column :accounts, :ssl_enabled, :boolean, :default => 1
# end
#
- # def self.down
+ # def down
# remove_column :accounts, :ssl_enabled
# end
# end
#
- # This migration will add a boolean flag to the accounts table and remove it
- # if you're backing out of the migration. It shows how all migrations have
- # two class methods +up+ and +down+ that describes the transformations
+ # This migration will add a boolean flag to the accounts table and remove it
+ # if you're backing out of the migration. It shows how all migrations have
+ # two class methods +up+ and +down+ that describes the transformations
# required to implement or remove the migration. These methods can consist
# of both the migration specific methods like add_column and remove_column,
- # but may also contain regular Ruby code for generating data needed for the
+ # but may also contain regular Ruby code for generating data needed for the
# transformations.
#
# Example of a more complex migration that also needs to initialize data:
#
# class AddSystemSettings < ActiveRecord::Migration
- # def self.up
+ # def up
# create_table :system_settings do |t|
# t.string :name
# t.string :label
@@ -72,58 +71,58 @@ module ActiveRecord
# t.integer :position
# end
#
- # SystemSetting.create :name => "notice",
- # :label => "Use notice?",
+ # SystemSetting.create :name => "notice",
+ # :label => "Use notice?",
# :value => 1
# end
#
- # def self.down
+ # def down
# drop_table :system_settings
# end
# end
#
- # This migration first adds the system_settings table, then creates the very
+ # This migration first adds the system_settings table, then creates the very
# first row in it using the Active Record model that relies on the table. It
- # also uses the more advanced create_table syntax where you can specify a
+ # also uses the more advanced create_table syntax where you can specify a
# complete table schema in one block call.
#
# == Available transformations
#
- # * <tt>create_table(name, options)</tt> Creates a table called +name+ and
+ # * <tt>create_table(name, options)</tt> Creates a table called +name+ and
# makes the table object available to a block that can then add columns to it,
- # following the same format as add_column. See example above. The options hash
- # is for fragments like "DEFAULT CHARSET=UTF-8" that are appended to the create
+ # following the same format as add_column. See example above. The options hash
+ # is for fragments like "DEFAULT CHARSET=UTF-8" that are appended to the create
# table definition.
# * <tt>drop_table(name)</tt>: Drops the table called +name+.
- # * <tt>rename_table(old_name, new_name)</tt>: Renames the table called +old_name+
+ # * <tt>rename_table(old_name, new_name)</tt>: Renames the table called +old_name+
# to +new_name+.
- # * <tt>add_column(table_name, column_name, type, options)</tt>: Adds a new column
+ # * <tt>add_column(table_name, column_name, type, options)</tt>: Adds a new column
# to the table called +table_name+
# named +column_name+ specified to be one of the following types:
- # <tt>:string</tt>, <tt>:text</tt>, <tt>:integer</tt>, <tt>:float</tt>,
+ # <tt>:string</tt>, <tt>:text</tt>, <tt>:integer</tt>, <tt>:float</tt>,
# <tt>:decimal</tt>, <tt>:datetime</tt>, <tt>:timestamp</tt>, <tt>:time</tt>,
# <tt>:date</tt>, <tt>:binary</tt>, <tt>:boolean</tt>. A default value can be
- # specified by passing an +options+ hash like <tt>{ :default => 11 }</tt>.
- # Other options include <tt>:limit</tt> and <tt>:null</tt> (e.g.
- # <tt>{ :limit => 50, :null => false }</tt>) -- see
+ # specified by passing an +options+ hash like <tt>{ :default => 11 }</tt>.
+ # Other options include <tt>:limit</tt> and <tt>:null</tt> (e.g.
+ # <tt>{ :limit => 50, :null => false }</tt>) -- see
# ActiveRecord::ConnectionAdapters::TableDefinition#column for details.
# * <tt>rename_column(table_name, column_name, new_column_name)</tt>: Renames
# a column but keeps the type and content.
- # * <tt>change_column(table_name, column_name, type, options)</tt>: Changes
+ # * <tt>change_column(table_name, column_name, type, options)</tt>: Changes
# the column to a different type using the same parameters as add_column.
- # * <tt>remove_column(table_name, column_name)</tt>: Removes the column named
+ # * <tt>remove_column(table_name, column_name)</tt>: Removes the column named
# +column_name+ from the table called +table_name+.
- # * <tt>add_index(table_name, column_names, options)</tt>: Adds a new index
+ # * <tt>add_index(table_name, column_names, options)</tt>: Adds a new index
# with the name of the column. Other options include
- # <tt>:name</tt> and <tt>:unique</tt> (e.g.
+ # <tt>:name</tt> and <tt>:unique</tt> (e.g.
# <tt>{ :name => "users_name_index", :unique => true }</tt>).
- # * <tt>remove_index(table_name, index_name)</tt>: Removes the index specified
+ # * <tt>remove_index(table_name, index_name)</tt>: Removes the index specified
# by +index_name+.
#
# == Irreversible transformations
#
- # Some transformations are destructive in a manner that cannot be reversed.
- # Migrations of that kind should raise an <tt>ActiveRecord::IrreversibleMigration</tt>
+ # Some transformations are destructive in a manner that cannot be reversed.
+ # Migrations of that kind should raise an <tt>ActiveRecord::IrreversibleMigration</tt>
# exception in their +down+ method.
#
# == Running migrations from within Rails
@@ -134,11 +133,11 @@ module ActiveRecord
# rails generate migration MyNewMigration
#
# where MyNewMigration is the name of your migration. The generator will
- # create an empty migration file <tt>timestamp_my_new_migration.rb</tt>
- # in the <tt>db/migrate/</tt> directory where <tt>timestamp</tt> is the
+ # create an empty migration file <tt>timestamp_my_new_migration.rb</tt>
+ # in the <tt>db/migrate/</tt> directory where <tt>timestamp</tt> is the
# UTC formatted date and time that the migration was generated.
#
- # You may then edit the <tt>self.up</tt> and <tt>self.down</tt> methods of
+ # You may then edit the <tt>up</tt> and <tt>down</tt> methods of
# MyNewMigration.
#
# There is a special syntactic shortcut to generate migrations that add fields to a table.
@@ -147,11 +146,11 @@ module ActiveRecord
#
# This will generate the file <tt>timestamp_add_fieldname_to_tablename</tt>, which will look like this:
# class AddFieldnameToTablename < ActiveRecord::Migration
- # def self.up
+ # def up
# add_column :tablenames, :fieldname, :string
# end
#
- # def self.down
+ # def down
# remove_column :tablenames, :fieldname
# end
# end
@@ -179,11 +178,11 @@ module ActiveRecord
# Not all migrations change the schema. Some just fix the data:
#
# class RemoveEmptyTags < ActiveRecord::Migration
- # def self.up
+ # def up
# Tag.find(:all).each { |tag| tag.destroy if tag.pages.empty? }
# end
#
- # def self.down
+ # def down
# # not much we can do to restore deleted data
# raise ActiveRecord::IrreversibleMigration, "Can't recover the deleted tags"
# end
@@ -192,12 +191,12 @@ module ActiveRecord
# Others remove columns when they migrate up instead of down:
#
# class RemoveUnnecessaryItemAttributes < ActiveRecord::Migration
- # def self.up
+ # def up
# remove_column :items, :incomplete_items_count
# remove_column :items, :completed_items_count
# end
#
- # def self.down
+ # def down
# add_column :items, :incomplete_items_count
# add_column :items, :completed_items_count
# end
@@ -206,24 +205,24 @@ module ActiveRecord
# And sometimes you need to do something in SQL not abstracted directly by migrations:
#
# class MakeJoinUnique < ActiveRecord::Migration
- # def self.up
+ # def up
# execute "ALTER TABLE `pages_linked_pages` ADD UNIQUE `page_id_linked_page_id` (`page_id`,`linked_page_id`)"
# end
#
- # def self.down
+ # def down
# execute "ALTER TABLE `pages_linked_pages` DROP INDEX `page_id_linked_page_id`"
# end
# end
#
# == Using a model after changing its table
#
- # Sometimes you'll want to add a column in a migration and populate it
- # immediately after. In that case, you'll need to make a call to
- # <tt>Base#reset_column_information</tt> in order to ensure that the model has the
+ # Sometimes you'll want to add a column in a migration and populate it
+ # immediately after. In that case, you'll need to make a call to
+ # <tt>Base#reset_column_information</tt> in order to ensure that the model has the
# latest column data from after the new column was added. Example:
#
# class AddPeopleSalary < ActiveRecord::Migration
- # def self.up
+ # def up
# add_column :people, :salary, :integer
# Person.reset_column_information
# Person.find(:all).each do |p|
@@ -243,7 +242,7 @@ module ActiveRecord
# You can also insert your own messages and benchmarks by using the +say_with_time+
# method:
#
- # def self.up
+ # def up
# ...
# say_with_time "Updating salaries..." do
# Person.find(:all).each do |p|
@@ -286,111 +285,216 @@ module ActiveRecord
#
# In application.rb.
#
+ # == Reversible Migrations
+ #
+ # Starting with Rails 3.1, you will be able to define reversible migrations.
+ # Reversible migrations are migrations that know how to go +down+ for you.
+ # You simply supply the +up+ logic, and the Migration system will figure out
+ # how to execute the down commands for you.
+ #
+ # To define a reversible migration, define the +change+ method in your
+ # migration like this:
+ #
+ # class TenderloveMigration < ActiveRecord::Migration
+ # def change
+ # create_table(:horses) do
+ # t.column :content, :text
+ # t.column :remind_at, :datetime
+ # end
+ # end
+ # end
+ #
+ # This migration will create the horses table for you on the way up, and
+ # automatically figure out how to drop the table on the way down.
+ #
+ # Some commands like +remove_column+ cannot be reversed. If you care to
+ # define how to move up and down in these cases, you should define the +up+
+ # and +down+ methods as before.
+ #
+ # If a command cannot be reversed, an
+ # <tt>ActiveRecord::IrreversibleMigration</tt> exception will be raised when
+ # the migration is moving down.
+ #
+ # For a list of commands that are reversible, please see
+ # <tt>ActiveRecord::Migration::CommandRecorder</tt>.
class Migration
- @@verbose = true
- cattr_accessor :verbose
+ autoload :CommandRecorder, 'active_record/migration/command_recorder'
class << self
- def up_with_benchmarks #:nodoc:
- migrate(:up)
- end
-
- def down_with_benchmarks #:nodoc:
- migrate(:down)
- end
+ attr_accessor :delegate # :nodoc:
+ end
- # Execute this migration in the named direction
- def migrate(direction)
- return unless respond_to?(direction)
+ def self.method_missing(name, *args, &block) # :nodoc:
+ (delegate || superclass.delegate).send(name, *args, &block)
+ end
- case direction
- when :up then announce "migrating"
- when :down then announce "reverting"
- end
+ cattr_accessor :verbose
- result = nil
- time = Benchmark.measure { result = send("#{direction}_without_benchmarks") }
+ attr_accessor :name, :version
- case direction
- when :up then announce "migrated (%.4fs)" % time.real; write
- when :down then announce "reverted (%.4fs)" % time.real; write
- end
+ def initialize
+ @name = self.class.name
+ @version = nil
+ @connection = nil
+ end
- result
- end
+ # instantiate the delegate object after initialize is defined
+ self.verbose = true
+ self.delegate = new
- # Because the method added may do an alias_method, it can be invoked
- # recursively. We use @ignore_new_methods as a guard to indicate whether
- # it is safe for the call to proceed.
- def singleton_method_added(sym) #:nodoc:
- return if defined?(@ignore_new_methods) && @ignore_new_methods
+ def up
+ self.class.delegate = self
+ return unless self.class.respond_to?(:up)
+ self.class.up
+ end
- begin
- @ignore_new_methods = true
+ def down
+ self.class.delegate = self
+ return unless self.class.respond_to?(:down)
+ self.class.down
+ end
- case sym
- when :up, :down
- singleton_class.send(:alias_method_chain, sym, "benchmarks")
+ # Execute this migration in the named direction
+ def migrate(direction)
+ return unless respond_to?(direction)
+
+ case direction
+ when :up then announce "migrating"
+ when :down then announce "reverting"
+ end
+
+ time = nil
+ ActiveRecord::Base.connection_pool.with_connection do |conn|
+ @connection = conn
+ if respond_to?(:change)
+ if direction == :down
+ recorder = CommandRecorder.new(@connection)
+ suppress_messages do
+ @connection = recorder
+ change
+ end
+ @connection = conn
+ time = Benchmark.measure {
+ recorder.inverse.each do |cmd, args|
+ send(cmd, *args)
+ end
+ }
+ else
+ time = Benchmark.measure { change }
end
- ensure
- @ignore_new_methods = false
+ else
+ time = Benchmark.measure { send(direction) }
end
+ @connection = nil
end
- def write(text="")
- puts(text) if verbose
+ case direction
+ when :up then announce "migrated (%.4fs)" % time.real; write
+ when :down then announce "reverted (%.4fs)" % time.real; write
end
+ end
- def announce(message)
- version = defined?(@version) ? @version : nil
+ def write(text="")
+ puts(text) if verbose
+ end
- text = "#{version} #{name}: #{message}"
- length = [0, 75 - text.length].max
- write "== %s %s" % [text, "=" * length]
- end
+ def announce(message)
+ text = "#{version} #{name}: #{message}"
+ length = [0, 75 - text.length].max
+ write "== %s %s" % [text, "=" * length]
+ end
- def say(message, subitem=false)
- write "#{subitem ? " ->" : "--"} #{message}"
- end
+ def say(message, subitem=false)
+ write "#{subitem ? " ->" : "--"} #{message}"
+ end
- def say_with_time(message)
- say(message)
- result = nil
- time = Benchmark.measure { result = yield }
- say "%.4fs" % time.real, :subitem
- say("#{result} rows", :subitem) if result.is_a?(Integer)
- result
- end
+ def say_with_time(message)
+ say(message)
+ result = nil
+ time = Benchmark.measure { result = yield }
+ say "%.4fs" % time.real, :subitem
+ say("#{result} rows", :subitem) if result.is_a?(Integer)
+ result
+ end
- def suppress_messages
- save, self.verbose = verbose, false
- yield
- ensure
- self.verbose = save
- end
+ def suppress_messages
+ save, self.verbose = verbose, false
+ yield
+ ensure
+ self.verbose = save
+ end
+
+ def connection
+ @connection || ActiveRecord::Base.connection
+ end
+
+ def method_missing(method, *arguments, &block)
+ arg_list = arguments.map{ |a| a.inspect } * ', '
- def connection
- ActiveRecord::Base.connection
+ say_with_time "#{method}(#{arg_list})" do
+ unless arguments.empty? || method == :execute
+ arguments[0] = Migrator.proper_table_name(arguments.first)
+ end
+ return super unless connection.respond_to?(method)
+ connection.send(method, *arguments, &block)
end
+ end
+
+ def copy(destination, sources, options = {})
+ copied = []
+
+ FileUtils.mkdir_p(destination) unless File.exists?(destination)
+
+ destination_migrations = ActiveRecord::Migrator.migrations(destination)
+ last = destination_migrations.last
+ sources.each do |name, path|
+ source_migrations = ActiveRecord::Migrator.migrations(path)
- def method_missing(method, *arguments, &block)
- arg_list = arguments.map(&:inspect) * ', '
+ source_migrations.each do |migration|
+ source = File.read(migration.filename)
+ source = "# This migration comes from #{name} (originally #{migration.version})\n#{source}"
- say_with_time "#{method}(#{arg_list})" do
- unless arguments.empty? || method == :execute
- arguments[0] = Migrator.proper_table_name(arguments.first)
+ if duplicate = destination_migrations.detect { |m| m.name == migration.name }
+ options[:on_skip].call(name, migration) if File.read(duplicate.filename) != source && options[:on_skip]
+ next
end
- connection.send(method, *arguments, &block)
+
+ migration.version = next_migration_number(last ? last.version + 1 : 0).to_i
+ new_path = File.join(destination, "#{migration.version}_#{migration.name.underscore}.rb")
+ old_path, migration.filename = migration.filename, new_path
+ last = migration
+
+ FileUtils.cp(old_path, migration.filename)
+ copied << migration
+ options[:on_copy].call(name, migration, old_path) if options[:on_copy]
+ destination_migrations << migration
end
end
+
+ copied
+ end
+
+ def next_migration_number(number)
+ if ActiveRecord::Base.timestamped_migrations
+ [Time.now.utc.strftime("%Y%m%d%H%M%S"), "%.14d" % number].max
+ else
+ "%.3d" % number
+ end
end
end
# MigrationProxy is used to defer loading of the actual migration classes
# until they are needed
- class MigrationProxy
+ class MigrationProxy < Struct.new(:name, :version, :filename)
- attr_accessor :name, :version, :filename
+ def initialize(name, version, filename)
+ super
+ @migration = nil
+ end
+
+ def basename
+ File.basename(filename)
+ end
delegate :migrate, :announce, :write, :to=>:migration
@@ -402,47 +506,47 @@ module ActiveRecord
def load_migration
require(File.expand_path(filename))
- name.constantize
+ name.constantize.new
end
end
class Migrator#:nodoc:
class << self
- def migrate(migrations_path, target_version = nil)
+ attr_writer :migrations_paths
+ alias :migrations_path= :migrations_paths=
+
+ def migrate(migrations_paths, target_version = nil)
case
when target_version.nil?
- up(migrations_path, target_version)
+ up(migrations_paths, target_version)
when current_version == 0 && target_version == 0
+ []
when current_version > target_version
- down(migrations_path, target_version)
+ down(migrations_paths, target_version)
else
- up(migrations_path, target_version)
+ up(migrations_paths, target_version)
end
end
- def rollback(migrations_path, steps=1)
- move(:down, migrations_path, steps)
+ def rollback(migrations_paths, steps=1)
+ move(:down, migrations_paths, steps)
end
- def forward(migrations_path, steps=1)
- move(:up, migrations_path, steps)
+ def forward(migrations_paths, steps=1)
+ move(:up, migrations_paths, steps)
end
- def up(migrations_path, target_version = nil)
- self.new(:up, migrations_path, target_version).migrate
+ def up(migrations_paths, target_version = nil)
+ self.new(:up, migrations_paths, target_version).migrate
end
- def down(migrations_path, target_version = nil)
- self.new(:down, migrations_path, target_version).migrate
+ def down(migrations_paths, target_version = nil)
+ self.new(:down, migrations_paths, target_version).migrate
end
- def run(direction, migrations_path, target_version)
- self.new(direction, migrations_path, target_version).run
- end
-
- def migrations_path
- 'db/migrate'
+ def run(direction, migrations_paths, target_version)
+ self.new(direction, migrations_paths, target_version).run
end
def schema_migrations_table_name
@@ -451,7 +555,7 @@ module ActiveRecord
def get_all_versions
table = Arel::Table.new(schema_migrations_table_name)
- Base.connection.select_values(table.project(table['version']).to_sql).map(&:to_i).sort
+ Base.connection.select_values(table.project(table['version']).to_sql).map{ |v| v.to_i }.sort
end
def current_version
@@ -468,24 +572,59 @@ module ActiveRecord
name.table_name rescue "#{ActiveRecord::Base.table_name_prefix}#{name}#{ActiveRecord::Base.table_name_suffix}"
end
+ def migrations_paths
+ @migrations_paths ||= ['db/migrate']
+ # just to not break things if someone uses: migration_path = some_string
+ Array.wrap(@migrations_paths)
+ end
+
+ def migrations_path
+ migrations_paths.first
+ end
+
+ def migrations(paths)
+ paths = Array.wrap(paths)
+
+ files = Dir[*paths.map { |p| "#{p}/[0-9]*_*.rb" }]
+
+ seen = Hash.new false
+
+ migrations = files.map do |file|
+ version, name = file.scan(/([0-9]+)_([_a-z0-9]*).rb/).first
+
+ raise IllegalMigrationNameError.new(file) unless version
+ version = version.to_i
+ name = name.camelize
+
+ raise DuplicateMigrationVersionError.new(version) if seen[version]
+ raise DuplicateMigrationNameError.new(name) if seen[name]
+
+ seen[version] = seen[name] = true
+
+ MigrationProxy.new(name, version, file)
+ end
+
+ migrations.sort_by(&:version)
+ end
+
private
- def move(direction, migrations_path, steps)
- migrator = self.new(direction, migrations_path)
+ def move(direction, migrations_paths, steps)
+ migrator = self.new(direction, migrations_paths)
start_index = migrator.migrations.index(migrator.current_migration)
if start_index
finish = migrator.migrations[start_index + steps]
version = finish ? finish.version : 0
- send(direction, migrations_path, version)
+ send(direction, migrations_paths, version)
end
end
end
- def initialize(direction, migrations_path, target_version = nil)
+ def initialize(direction, migrations_paths, target_version = nil)
raise StandardError.new("This database does not yet support migrations") unless Base.connection.supports_migrations?
Base.connection.initialize_schema_migrations_table
- @direction, @migrations_path, @target_version = direction, migrations_path, target_version
+ @direction, @migrations_paths, @target_version = direction, migrations_paths, target_version
end
def current_version
@@ -509,7 +648,7 @@ module ActiveRecord
current = migrations.detect { |m| m.version == current_version }
target = migrations.detect { |m| m.version == @target_version }
- if target.nil? && !@target_version.nil? && @target_version > 0
+ if target.nil? && @target_version && @target_version > 0
raise UnknownMigrationVersionError.new(@target_version)
end
@@ -518,16 +657,19 @@ module ActiveRecord
runnable = migrations[start..finish]
# skip the last migration if we're headed down, but not ALL the way down
- runnable.pop if down? && !target.nil?
+ runnable.pop if down? && target
+ ran = []
runnable.each do |migration|
Base.logger.info "Migrating to #{migration.name} (#{migration.version})" if Base.logger
+ seen = migrated.include?(migration.version.to_i)
+
# On our way up, we skip migrating the ones we've already migrated
- next if up? && migrated.include?(migration.version.to_i)
+ next if up? && seen
# On our way down, we skip reverting the ones we've never migrated
- if down? && !migrated.include?(migration.version.to_i)
+ if down? && !seen
migration.announce 'never migrated, skipping'; migration.write
next
end
@@ -537,39 +679,18 @@ module ActiveRecord
migration.migrate(@direction)
record_version_state_after_migrating(migration.version)
end
+ ran << migration
rescue => e
canceled_msg = Base.connection.supports_ddl_transactions? ? "this and " : ""
raise StandardError, "An error has occurred, #{canceled_msg}all later migrations canceled:\n\n#{e}", e.backtrace
end
end
+ ran
end
def migrations
@migrations ||= begin
- files = Dir["#{@migrations_path}/[0-9]*_*.rb"]
-
- migrations = files.inject([]) do |klasses, file|
- version, name = file.scan(/([0-9]+)_([_a-z0-9]*).rb/).first
-
- raise IllegalMigrationNameError.new(file) unless version
- version = version.to_i
-
- if klasses.detect { |m| m.version == version }
- raise DuplicateMigrationVersionError.new(version)
- end
-
- if klasses.detect { |m| m.name == name.camelize }
- raise DuplicateMigrationNameError.new(name.camelize)
- end
-
- migration = MigrationProxy.new
- migration.name = name.camelize
- migration.version = version
- migration.filename = file
- klasses << migration
- end
-
- migrations = migrations.sort_by(&:version)
+ migrations = self.class.migrations(@migrations_paths)
down? ? migrations.reverse : migrations
end
end
@@ -590,10 +711,12 @@ module ActiveRecord
@migrated_versions ||= []
if down?
@migrated_versions.delete(version)
- table.where(table["version"].eq(version.to_s)).delete
+ stmt = table.where(table["version"].eq(version.to_s)).compile_delete
+ Base.connection.delete stmt.to_sql
else
@migrated_versions.push(version).sort!
- table.insert table["version"] => version.to_s
+ stmt = table.compile_insert table["version"] => version.to_s
+ Base.connection.insert stmt.to_sql
end
end
diff --git a/activerecord/lib/active_record/migration/command_recorder.rb b/activerecord/lib/active_record/migration/command_recorder.rb
new file mode 100644
index 0000000000..c9d57ce812
--- /dev/null
+++ b/activerecord/lib/active_record/migration/command_recorder.rb
@@ -0,0 +1,103 @@
+module ActiveRecord
+ class Migration
+ # ActiveRecord::Migration::CommandRecorder records commands done during
+ # a migration and knows how to reverse those commands. The CommandRecorder
+ # knows how to invert the following commands:
+ #
+ # * add_column
+ # * add_index
+ # * add_timestamp
+ # * create_table
+ # * remove_timestamps
+ # * rename_column
+ # * rename_index
+ # * rename_table
+ class CommandRecorder
+ attr_accessor :commands, :delegate
+
+ def initialize(delegate = nil)
+ @commands = []
+ @delegate = delegate
+ end
+
+ # record +command+. +command+ should be a method name and arguments.
+ # For example:
+ #
+ # recorder.record(:method_name, [:arg1, arg2])
+ def record(*command)
+ @commands << command
+ end
+
+ # Returns a list that represents commands that are the inverse of the
+ # commands stored in +commands+. For example:
+ #
+ # recorder.record(:rename_table, [:old, :new])
+ # recorder.inverse # => [:rename_table, [:new, :old]]
+ #
+ # This method will raise an IrreversibleMigration exception if it cannot
+ # invert the +commands+.
+ def inverse
+ @commands.reverse.map { |name, args|
+ method = :"invert_#{name}"
+ raise IrreversibleMigration unless respond_to?(method, true)
+ send(method, args)
+ }
+ end
+
+ def respond_to?(*args) # :nodoc:
+ super || delegate.respond_to?(*args)
+ end
+
+ [:create_table, :rename_table, :add_column, :remove_column, :rename_index, :rename_column, :add_index, :remove_index, :add_timestamps, :remove_timestamps, :change_column, :change_column_default].each do |method|
+ class_eval <<-EOV, __FILE__, __LINE__ + 1
+ def #{method}(*args)
+ record(:"#{method}", args)
+ end
+ EOV
+ end
+
+ private
+
+ def invert_create_table(args)
+ [:drop_table, args]
+ end
+
+ def invert_rename_table(args)
+ [:rename_table, args.reverse]
+ end
+
+ def invert_add_column(args)
+ [:remove_column, args.first(2)]
+ end
+
+ def invert_rename_index(args)
+ [:rename_index, args.reverse]
+ end
+
+ def invert_rename_column(args)
+ [:rename_column, [args.first] + args.last(2).reverse]
+ end
+
+ def invert_add_index(args)
+ table, columns, _ = *args
+ [:remove_index, [table, {:column => columns}]]
+ end
+
+ def invert_remove_timestamps(args)
+ [:add_timestamps, args]
+ end
+
+ def invert_add_timestamps(args)
+ [:remove_timestamps, args]
+ end
+
+ # Forwards any missing method call to the \target.
+ def method_missing(method, *args, &block)
+ @delegate.send(method, *args, &block)
+ rescue NoMethodError => e
+ raise e, e.message.sub(/ for #<.*$/, " via proxy for #{@delegate}")
+ end
+
+ end
+ end
+end
diff --git a/activerecord/lib/active_record/named_scope.rb b/activerecord/lib/active_record/named_scope.rb
index 0e560418dc..d291632260 100644
--- a/activerecord/lib/active_record/named_scope.rb
+++ b/activerecord/lib/active_record/named_scope.rb
@@ -2,12 +2,18 @@ require 'active_support/core_ext/array'
require 'active_support/core_ext/hash/except'
require 'active_support/core_ext/kernel/singleton_class'
require 'active_support/core_ext/object/blank'
+require 'active_support/core_ext/class/attribute'
module ActiveRecord
# = Active Record Named \Scopes
module NamedScope
extend ActiveSupport::Concern
+ included do
+ class_attribute :scopes
+ self.scopes = {}
+ end
+
module ClassMethods
# Returns an anonymous \scope.
#
@@ -20,10 +26,10 @@ module ActiveRecord
# fruits = fruits.limit(10) if limited?
#
# Anonymous \scopes tend to be useful when procedurally generating complex
- # queries, where passing intermediate values (\scopes) around as first-class
+ # queries, where passing intermediate values (\scopes) around as first-class
# objects is convenient.
#
- # You can define a \scope that applies to all finders using
+ # You can define a \scope that applies to all finders using
# ActiveRecord::Base.default_scope.
def scoped(options = nil)
if options
@@ -33,10 +39,6 @@ module ActiveRecord
end
end
- def scopes
- read_inheritable_attribute(:scopes) || write_inheritable_attribute(:scopes, {})
- end
-
# Adds a class method for retrieving and querying objects. A \scope represents a narrowing of a database query,
# such as <tt>where(:color => :red).select('shirts.*').includes(:washing_instructions)</tt>.
#
@@ -48,20 +50,20 @@ module ActiveRecord
# The above calls to <tt>scope</tt> define class methods Shirt.red and Shirt.dry_clean_only. Shirt.red,
# in effect, represents the query <tt>Shirt.where(:color => 'red')</tt>.
#
- # Unlike <tt>Shirt.find(...)</tt>, however, the object returned by Shirt.red is not an Array; it
- # resembles the association object constructed by a <tt>has_many</tt> declaration. For instance,
- # you can invoke <tt>Shirt.red.first</tt>, <tt>Shirt.red.count</tt>, <tt>Shirt.red.where(:size => 'small')</tt>.
- # Also, just as with the association objects, named \scopes act like an Array, implementing Enumerable;
+ # Unlike <tt>Shirt.find(...)</tt>, however, the object returned by Shirt.red is not an Array; it
+ # resembles the association object constructed by a <tt>has_many</tt> declaration. For instance,
+ # you can invoke <tt>Shirt.red.first</tt>, <tt>Shirt.red.count</tt>, <tt>Shirt.red.where(:size => 'small')</tt>.
+ # Also, just as with the association objects, named \scopes act like an Array, implementing Enumerable;
# <tt>Shirt.red.each(&block)</tt>, <tt>Shirt.red.first</tt>, and <tt>Shirt.red.inject(memo, &block)</tt>
# all behave as if Shirt.red really was an Array.
#
- # These named \scopes are composable. For instance, <tt>Shirt.red.dry_clean_only</tt> will produce
+ # These named \scopes are composable. For instance, <tt>Shirt.red.dry_clean_only</tt> will produce
# all shirts that are both red and dry clean only.
- # Nested finds and calculations also work with these compositions: <tt>Shirt.red.dry_clean_only.count</tt>
- # returns the number of garments for which these criteria obtain. Similarly with
+ # Nested finds and calculations also work with these compositions: <tt>Shirt.red.dry_clean_only.count</tt>
+ # returns the number of garments for which these criteria obtain. Similarly with
# <tt>Shirt.red.dry_clean_only.average(:thread_count)</tt>.
#
- # All \scopes are available as class methods on the ActiveRecord::Base descendant upon which
+ # All \scopes are available as class methods on the ActiveRecord::Base descendant upon which
# the \scopes were defined. But they are also available to <tt>has_many</tt> associations. If,
#
# class Person < ActiveRecord::Base
@@ -88,14 +90,22 @@ module ActiveRecord
# end
# end
# end
- def scope(name, scope_options = {}, &block)
+ #
+ # Scopes can also be used while creating/building a record.
+ #
+ # class Article < ActiveRecord::Base
+ # scope :published, where(:published => true)
+ # end
+ #
+ # Article.published.new.published # => true
+ # Article.published.create.published # => true
+ def scope(name, scope_options = {})
name = name.to_sym
valid_scope_name?(name)
+ extension = Module.new(&Proc.new) if block_given?
- extension = Module.new(&block) if block_given?
-
- scopes[name] = lambda do |*args|
- options = scope_options.is_a?(Proc) ? scope_options.call(*args) : scope_options
+ scope_proc = lambda do |*args|
+ options = scope_options.respond_to?(:call) ? scope_options.call(*args) : scope_options
relation = if options.is_a?(Hash)
scoped.apply_finder_options(options)
@@ -108,12 +118,9 @@ module ActiveRecord
extension ? relation.extending(extension) : relation
end
- singleton_class.send(:redefine_method, name, &scopes[name])
- end
+ self.scopes = self.scopes.merge name => scope_proc
- def named_scope(*args, &block)
- ActiveSupport::Deprecation.warn("Base.named_scope has been deprecated, please use Base.scope instead", caller)
- scope(*args, &block)
+ singleton_class.send(:redefine_method, name, &scopes[name])
end
protected
diff --git a/activerecord/lib/active_record/nested_attributes.rb b/activerecord/lib/active_record/nested_attributes.rb
index e652296e2c..522c0cfc9f 100644
--- a/activerecord/lib/active_record/nested_attributes.rb
+++ b/activerecord/lib/active_record/nested_attributes.rb
@@ -2,6 +2,7 @@ require 'active_support/core_ext/hash/except'
require 'active_support/core_ext/object/try'
require 'active_support/core_ext/object/blank'
require 'active_support/core_ext/hash/indifferent_access'
+require 'active_support/core_ext/class/attribute'
module ActiveRecord
module NestedAttributes #:nodoc:
@@ -11,7 +12,7 @@ module ActiveRecord
extend ActiveSupport::Concern
included do
- class_inheritable_accessor :nested_attributes_options, :instance_writer => false
+ class_attribute :nested_attributes_options, :instance_writer => false
self.nested_attributes_options = {}
end
@@ -25,7 +26,7 @@ module ActiveRecord
#
# 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>.
#
@@ -190,6 +191,34 @@ module ActiveRecord
# 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.
+ #
+ # === Using with attr_accessible
+ #
+ # The use of <tt>attr_accessible</tt> can interfere with nested attributes
+ # if you're not careful. For example, if the <tt>Member</tt> model above
+ # was using <tt>attr_accessible</tt> like this:
+ #
+ # attr_accessible :name
+ #
+ # You would need to modify it to look like this:
+ #
+ # attr_accessible :name, :posts_attributes
+ #
+ # === Validating the presence of a parent model
+ #
+ # If you want to validate that a child record is associated with a parent
+ # record, you can use <tt>validates_presence_of</tt> and
+ # <tt>inverse_of</tt> as this example illustrates:
+ #
+ # class Member < ActiveRecord::Base
+ # has_many :posts, :inverse_of => :member
+ # accepts_nested_attributes_for :posts
+ # end
+ #
+ # class Post < ActiveRecord::Base
+ # belongs_to :member, :inverse_of => :posts
+ # validates_presence_of :member
+ # end
module ClassMethods
REJECT_ALL_BLANK_PROC = proc { |attributes| attributes.all? { |_, value| value.blank? } }
@@ -240,7 +269,11 @@ module ActiveRecord
if reflection = reflect_on_association(association_name)
reflection.options[:autosave] = true
add_autosave_association_callbacks(reflection)
+
+ nested_attributes_options = self.nested_attributes_options.dup
nested_attributes_options[association_name.to_sym] = options
+ self.nested_attributes_options = nested_attributes_options
+
type = (reflection.collection? ? :collection : :one_to_one)
# def pirate_attributes=(attributes)
@@ -287,18 +320,15 @@ module ActiveRecord
# update_only is true, and a <tt>:_destroy</tt> key set to a truthy value,
# then the existing record will be marked for destruction.
def assign_nested_attributes_for_one_to_one_association(association_name, attributes)
- options = nested_attributes_options[association_name]
+ options = self.nested_attributes_options[association_name]
attributes = attributes.with_indifferent_access
- check_existing_record = (options[:update_only] || !attributes['id'].blank?)
- if check_existing_record && (record = send(association_name)) &&
+ if (options[:update_only] || !attributes['id'].blank?) && (record = send(association_name)) &&
(options[:update_only] || record.id.to_s == attributes['id'].to_s)
- assign_to_or_mark_for_destruction(record, attributes, options[:allow_destroy])
+ assign_to_or_mark_for_destruction(record, attributes, options[:allow_destroy]) unless call_reject_if(association_name, attributes)
- elsif attributes['id']
- existing_record = self.class.reflect_on_association(association_name).klass.find(attributes['id'])
- assign_to_or_mark_for_destruction(existing_record, attributes, options[:allow_destroy])
- self.send(association_name.to_s+'=', existing_record)
+ elsif attributes['id'].present?
+ raise_nested_attributes_record_not_found(association_name, attributes['id'])
elsif !reject_new_record?(association_name, attributes)
method = "build_#{association_name}"
@@ -338,7 +368,7 @@ module ActiveRecord
# { :id => '2', :_destroy => true }
# ])
def assign_nested_attributes_for_collection_association(association_name, attributes_collection)
- options = nested_attributes_options[association_name]
+ options = self.nested_attributes_options[association_name]
unless attributes_collection.is_a?(Hash) || attributes_collection.is_a?(Array)
raise ArgumentError, "Hash or Array expected, got #{attributes_collection.class.name} (#{attributes_collection.inspect})"
@@ -349,16 +379,21 @@ module ActiveRecord
end
if attributes_collection.is_a? Hash
- attributes_collection = attributes_collection.sort_by { |index, _| index.to_i }.map { |_, attributes| attributes }
+ keys = attributes_collection.keys
+ attributes_collection = if keys.include?('id') || keys.include?(:id)
+ Array.wrap(attributes_collection)
+ else
+ attributes_collection.sort_by { |i, _| i.to_i }.map { |_, attributes| attributes }
+ end
end
- association = send(association_name)
+ association = association(association_name)
existing_records = if association.loaded?
- association.to_a
+ association.target
else
attribute_ids = attributes_collection.map {|a| a['id'] || a[:id] }.compact
- attribute_ids.present? ? association.all(:conditions => {association.primary_key => attribute_ids}) : []
+ attribute_ids.empty? ? [] : association.scoped.where(association.klass.primary_key => attribute_ids)
end
attributes_collection.each do |attributes|
@@ -368,16 +403,31 @@ module ActiveRecord
unless reject_new_record?(association_name, attributes)
association.build(attributes.except(*UNASSIGNABLE_KEYS))
end
-
elsif existing_records.count == 0 #Existing record but not yet associated
existing_record = self.class.reflect_on_association(association_name).klass.find(attributes['id'])
- association.send(:add_record_to_target_with_callbacks, existing_record) unless association.loaded?
- assign_to_or_mark_for_destruction(existing_record, attributes, options[:allow_destroy])
-
+ if !call_reject_if(association_name, attributes)
+ association.send(:add_record_to_target_with_callbacks, existing_record) if !association.loaded?
+ assign_to_or_mark_for_destruction(existing_record, attributes, options[:allow_destroy])
+ end
elsif existing_record = existing_records.detect { |record| record.id.to_s == attributes['id'].to_s }
- association.send(:add_record_to_target_with_callbacks, existing_record) unless association.loaded?
- assign_to_or_mark_for_destruction(existing_record, attributes, options[:allow_destroy])
+ unless association.loaded? || call_reject_if(association_name, attributes)
+ # Make sure we are operating on the actual object which is in the association's
+ # proxy_target array (either by finding it, or adding it if not found)
+ target_record = association.target.detect { |record| record == existing_record }
+
+ if target_record
+ existing_record = target_record
+ else
+ association.add_to_target(existing_record)
+ end
+ end
+
+ if !call_reject_if(association_name, attributes)
+ assign_to_or_mark_for_destruction(existing_record, attributes, options[:allow_destroy])
+ end
+ else
+ raise_nested_attributes_record_not_found(association_name, attributes['id'])
end
end
end
@@ -385,11 +435,8 @@ module ActiveRecord
# Updates a record with the +attributes+ or marks it for destruction if
# +allow_destroy+ is +true+ and has_destroy_flag? returns +true+.
def assign_to_or_mark_for_destruction(record, attributes, allow_destroy)
- if has_destroy_flag?(attributes) && allow_destroy
- record.mark_for_destruction
- else
- record.attributes = attributes.except(*UNASSIGNABLE_KEYS)
- end
+ record.attributes = attributes.except(*UNASSIGNABLE_KEYS)
+ record.mark_for_destruction if has_destroy_flag?(attributes) && allow_destroy
end
# Determines if a hash contains a truthy _destroy key.
@@ -397,7 +444,7 @@ module ActiveRecord
ConnectionAdapters::Column.value_to_boolean(hash['_destroy'])
end
- # Determines if a new record should be built by checking for
+ # Determines if a new record should be build by checking for
# has_destroy_flag? or if a <tt>:reject_if</tt> proc exists for this
# association and evaluates to +true+.
def reject_new_record?(association_name, attributes)
@@ -405,7 +452,7 @@ module ActiveRecord
end
def call_reject_if(association_name, attributes)
- case callback = nested_attributes_options[association_name][:reject_if]
+ case callback = self.nested_attributes_options[association_name][:reject_if]
when Symbol
method(callback).arity == 0 ? send(callback) : send(callback, attributes)
when Proc
@@ -413,5 +460,8 @@ module ActiveRecord
end
end
+ def raise_nested_attributes_record_not_found(association_name, record_id)
+ raise RecordNotFound, "Couldn't find #{self.class.reflect_on_association(association_name).klass.name} with ID=#{record_id} for #{self.class.name} with ID=#{id}"
+ end
end
end
diff --git a/activerecord/lib/active_record/observer.rb b/activerecord/lib/active_record/observer.rb
index 78bac55bf2..0893d7e337 100644
--- a/activerecord/lib/active_record/observer.rb
+++ b/activerecord/lib/active_record/observer.rb
@@ -3,7 +3,7 @@ require 'active_support/core_ext/class/attribute'
module ActiveRecord
# = Active Record Observer
#
- # Observer classes respond to lifecycle callbacks to implement trigger-like
+ # Observer classes respond to life cycle callbacks to implement trigger-like
# behavior outside the original class. This is a great way to reduce the
# clutter that normally comes when the model class is burdened with
# functionality that doesn't pertain to the core responsibility of the
@@ -67,7 +67,7 @@ module ActiveRecord
#
# == Configuration
#
- # In order to activate an observer, list it in the <tt>config.active_record.observers</tt> configuration
+ # In order to activate an observer, list it in the <tt>config.active_record.observers</tt> configuration
# setting in your <tt>config/application.rb</tt> file.
#
# config.active_record.observers = :comment_observer, :signup_observer
@@ -89,50 +89,31 @@ module ActiveRecord
# singletons and that call instantiates and registers them.
#
class Observer < ActiveModel::Observer
- class_attribute :observed_methods
- self.observed_methods = [].freeze
-
- def initialize
- super
- observed_descendants.each { |klass| add_observer!(klass) }
- end
-
- def self.method_added(method)
- method = method.to_sym
-
- if ActiveRecord::Callbacks::CALLBACKS.include?(method)
- self.observed_methods += [method]
- self.observed_methods.freeze
- end
- end
protected
- def observed_descendants
- observed_classes.sum([]) { |klass| klass.descendants }
- end
-
- def observe_callbacks?
- self.class.observed_methods.any?
+ def observed_classes
+ klasses = super
+ klasses + klasses.map { |klass| klass.descendants }.flatten
end
def add_observer!(klass)
super
- define_callbacks klass if observe_callbacks?
+ define_callbacks klass
end
def define_callbacks(klass)
- existing_methods = klass.instance_methods.map(&:to_sym)
observer = self
observer_name = observer.class.name.underscore.gsub('/', '__')
- self.class.observed_methods.each do |method|
- callback = :"_notify_#{observer_name}_for_#{method}"
- unless existing_methods.include? callback
- klass.send(:define_method, callback) do # def _notify_user_observer_for_before_save
- observer.update(method, self) # observer.update(:before_save, self)
- end # end
- klass.send(method, callback) # before_save :_notify_user_observer_for_before_save
+ ActiveRecord::Callbacks::CALLBACKS.each do |callback|
+ next unless respond_to?(callback)
+ callback_meth = :"_notify_#{observer_name}_for_#{callback}"
+ unless klass.respond_to?(callback_meth)
+ klass.send(:define_method, callback_meth) do
+ observer.send(callback, self)
+ end
+ klass.send(callback, callback_meth)
end
end
end
diff --git a/activerecord/lib/active_record/persistence.rb b/activerecord/lib/active_record/persistence.rb
index 71b46beaef..17a64b6e86 100644
--- a/activerecord/lib/active_record/persistence.rb
+++ b/activerecord/lib/active_record/persistence.rb
@@ -1,7 +1,7 @@
module ActiveRecord
# = Active Record Persistence
module Persistence
- # Returns true if this object hasn't been saved yet -- that is, a record
+ # Returns true if this object hasn't been saved yet -- that is, a record
# for the object doesn't exist in the data store yet; otherwise, returns false.
def new_record?
@new_record
@@ -18,9 +18,6 @@ module ActiveRecord
!(new_record? || destroyed?)
end
- # :call-seq:
- # save(options)
- #
# Saves the model.
#
# If the model is new a record gets created in the database, otherwise
@@ -67,15 +64,19 @@ module ActiveRecord
# callbacks, Observer methods, or any <tt>:dependent</tt> association
# options, use <tt>#destroy</tt>.
def delete
- self.class.delete(id) if persisted?
+ if persisted?
+ self.class.delete(id)
+ IdentityMap.remove(self) if IdentityMap.enabled?
+ end
@destroyed = true
freeze
end
- # Deletes the record in the database and freezes this instance to reflect
+ # Deletes the record in the database and freezes this instance to reflect
# that no changes should be made (since they can't be persisted).
def destroy
if persisted?
+ IdentityMap.remove(self) if IdentityMap.enabled?
self.class.unscoped.where(self.class.arel_table[self.class.primary_key].eq(id)).delete_all
end
@@ -83,15 +84,15 @@ module ActiveRecord
freeze
end
- # Returns an instance of the specified +klass+ with the attributes of the
- # current record. This is mostly useful in relation to single-table
- # inheritance structures where you want a subclass to appear as the
- # superclass. This can be used along with record identification in
- # Action Pack to allow, say, <tt>Client < Company</tt> to do something
+ # Returns an instance of the specified +klass+ with the attributes of the
+ # current record. This is mostly useful in relation to single-table
+ # inheritance structures where you want a subclass to appear as the
+ # superclass. This can be used along with record identification in
+ # Action Pack to allow, say, <tt>Client < Company</tt> to do something
# like render <tt>:partial => @client.becomes(Company)</tt> to render that
# instance using the companies/company partial instead of clients/client.
#
- # Note: The new instance will share a link to the same attributes as the original class.
+ # Note: The new instance will share a link to the same attributes as the original class.
# So any change to the attributes in either instance will affect the other.
def becomes(klass)
became = klass.new
@@ -99,37 +100,23 @@ module ActiveRecord
became.instance_variable_set("@attributes_cache", @attributes_cache)
became.instance_variable_set("@new_record", new_record?)
became.instance_variable_set("@destroyed", destroyed?)
+ became.type = klass.name unless self.class.descends_from_active_record?
became
end
- # Updates a single attribute and saves the record.
+ # Updates a single attribute and saves the record.
# This is especially useful for boolean flags on existing records. Also note that
#
- # * The attribute being updated must be a column name.
# * Validation is skipped.
- # * No callbacks are invoked.
+ # * Callbacks are invoked.
# * updated_at/updated_on column is updated if that column is available.
- # * Does not work on associations.
- # * Does not work on attr_accessor attributes.
- # * Does not work on new record. <tt>record.new_record?</tt> should return false for this method to work.
- # * Updates only the attribute that is input to the method. If there are other changed attributes then
- # those attributes are left alone. In that case even after this method has done its work <tt>record.changed?</tt>
- # will return true.
+ # * Updates all the attributes that are dirty in this object.
#
def update_attribute(name, value)
- raise ActiveRecordError, "#{name.to_s} is marked as readonly" if self.class.readonly_attributes.include? name.to_s
-
- changes = record_update_timestamps || {}
-
- if name
- name = name.to_s
- send("#{name}=", value)
- changes[name] = read_attribute(name)
- end
-
- @changed_attributes.except!(*changes.keys)
- primary_key = self.class.primary_key
- self.class.update_all(changes, { primary_key => self[primary_key] }) == 1
+ name = name.to_s
+ raise ActiveRecordError, "#{name} is marked as readonly" if self.class.readonly_attributes.include?(name)
+ send("#{name}=", value)
+ save(:validate => false)
end
# Updates the attributes of the model from the passed-in hash and saves the
@@ -213,22 +200,54 @@ module ActiveRecord
def reload(options = nil)
clear_aggregation_cache
clear_association_cache
- @attributes.update(self.class.unscoped { self.class.find(self.id, options) }.instance_variable_get('@attributes'))
+
+ IdentityMap.without do
+ fresh_object = self.class.unscoped { self.class.find(self.id, options) }
+ @attributes.update(fresh_object.instance_variable_get('@attributes'))
+ end
+
@attributes_cache = {}
self
end
# Saves the record with the updated_at/on attributes set to the current time.
# Please note that no validation is performed and no callbacks are executed.
- # If an attribute name is passed, that attribute is updated along with
+ # If an attribute name is passed, that attribute is updated along with
# updated_at/on attributes.
#
- # Examples:
- #
# product.touch # updates updated_at/on
# product.touch(:designed_at) # updates the designed_at attribute and updated_at/on
- def touch(attribute = nil)
- update_attribute(attribute, current_time_from_proper_timezone)
+ #
+ # If used along with +belongs_to+ then +touch+ will invoke +touch+ method on associated object.
+ #
+ # class Brake < ActiveRecord::Base
+ # belongs_to :car, :touch => true
+ # end
+ #
+ # class Car < ActiveRecord::Base
+ # belongs_to :corporation, :touch => true
+ # end
+ #
+ # # triggers @brake.car.touch and @brake.car.corporation.touch
+ # @brake.touch
+ def touch(name = nil)
+ attributes = timestamp_attributes_for_update_in_model
+ attributes << name if name
+
+ unless attributes.empty?
+ current_time = current_time_from_proper_timezone
+ changes = {}
+
+ attributes.each do |column|
+ changes[column.to_s] = write_attribute(column.to_s, current_time)
+ end
+
+ changes[self.class.locking_column] = increment_lock if locking_enabled?
+
+ @changed_attributes.except!(*changes.keys)
+ primary_key = self.class.primary_key
+ self.class.update_all(changes, { primary_key => self[primary_key] }) == 1
+ end
end
private
@@ -243,26 +262,21 @@ module ActiveRecord
def update(attribute_names = @attributes.keys)
attributes_with_values = arel_attributes_values(false, false, attribute_names)
return 0 if attributes_with_values.empty?
- self.class.unscoped.where(self.class.arel_table[self.class.primary_key].eq(id)).arel.update(attributes_with_values)
+ klass = self.class
+ stmt = klass.unscoped.where(klass.arel_table[klass.primary_key].eq(id)).arel.compile_update(attributes_with_values)
+ klass.connection.update stmt.to_sql
end
# Creates a record with values matching those of the instance attributes
# and returns its id.
def create
- if self.id.nil? && connection.prefetch_primary_key?(self.class.table_name)
- self.id = connection.next_sequence_value(self.class.sequence_name)
- end
+ attributes_values = arel_attributes_values(!id.nil?)
- attributes_values = arel_attributes_values
-
- new_id = if attributes_values.empty?
- self.class.unscoped.insert connection.empty_insert_statement_value
- else
- self.class.unscoped.insert attributes_values
- end
+ new_id = self.class.unscoped.insert attributes_values
self.id ||= new_id
+ IdentityMap.add(self) if IdentityMap.enabled?
@new_record = false
id
end
@@ -272,10 +286,9 @@ module ActiveRecord
# that a new instance, or one populated from a passed-in Hash, still has all the attributes
# that instances loaded from the database would.
def attributes_from_column_definition
- self.class.columns.inject({}) do |attributes, column|
- attributes[column.name] = column.default unless column.name == self.class.primary_key
- attributes
- end
+ Hash[self.class.columns.map do |column|
+ [column.name, column.default]
+ end]
end
end
end
diff --git a/activerecord/lib/active_record/railtie.rb b/activerecord/lib/active_record/railtie.rb
index 78fdb77216..cace6f0cc0 100644
--- a/activerecord/lib/active_record/railtie.rb
+++ b/activerecord/lib/active_record/railtie.rb
@@ -13,10 +13,14 @@ module ActiveRecord
class Railtie < Rails::Railtie
config.active_record = ActiveSupport::OrderedOptions.new
- config.generators.orm :active_record, :migration => true,
- :timestamps => true
+ config.app_generators.orm :active_record, :migration => true,
+ :timestamps => true
- config.app_middleware.insert_after "::ActionDispatch::Callbacks", "ActiveRecord::QueryCache"
+ config.app_middleware.insert_after "::ActionDispatch::Callbacks",
+ "ActiveRecord::QueryCache"
+
+ config.app_middleware.insert_after "::ActionDispatch::Callbacks",
+ "ActiveRecord::ConnectionAdapters::ConnectionManagement"
rake_tasks do
load "active_record/railties/databases.rake"
@@ -39,6 +43,11 @@ module ActiveRecord
ActiveSupport.on_load(:active_record) { self.logger ||= ::Rails.logger }
end
+ initializer "active_record.identity_map" do |app|
+ config.app_middleware.insert_after "::ActionDispatch::Callbacks",
+ "ActiveRecord::IdentityMap::Middleware" if config.active_record.delete(:identity_map)
+ end
+
initializer "active_record.set_configs" do |app|
ActiveSupport.on_load(:active_record) do
app.config.active_record.each do |k,v|
@@ -65,27 +74,19 @@ module ActiveRecord
end
initializer "active_record.set_dispatch_hooks", :before => :set_clear_dependencies_hook do |app|
- unless app.config.cache_classes
- ActiveSupport.on_load(:active_record) do
- ActionDispatch::Callbacks.after do
- ActiveRecord::Base.clear_reloadable_connections!
- end
+ ActiveSupport.on_load(:active_record) do
+ ActionDispatch::Reloader.to_cleanup do
+ ActiveRecord::Base.clear_reloadable_connections!
+ ActiveRecord::Base.clear_cache!
end
end
end
- initializer "active_record.add_concurrency_middleware" do |app|
- if app.config.allow_concurrency
- app.config.middleware.insert_after "::ActionDispatch::Callbacks",
- "ActiveRecord::ConnectionAdapters::ConnectionManagement"
- end
- end
-
config.after_initialize do
ActiveSupport.on_load(:active_record) do
instantiate_observers
- ActionDispatch::Callbacks.to_prepare(:activerecord_instantiate_observers) do
+ ActionDispatch::Reloader.to_prepare do
ActiveRecord::Base.instantiate_observers
end
end
diff --git a/activerecord/lib/active_record/railties/databases.rake b/activerecord/lib/active_record/railties/databases.rake
index ae605d3e7a..ff36814684 100644
--- a/activerecord/lib/active_record/railties/databases.rake
+++ b/activerecord/lib/active_record/railties/databases.rake
@@ -1,7 +1,14 @@
-namespace :db do
+db_namespace = namespace :db do
task :load_config => :rails_env do
require 'active_record'
ActiveRecord::Base.configurations = Rails.application.config.database_configuration
+ ActiveRecord::Migrator.migrations_paths = Rails.application.paths["db/migrate"].to_a
+
+ if defined?(ENGINE_PATH) && engine = Rails::Engine.find(ENGINE_PATH)
+ if engine.paths["db/migrate"].existent
+ ActiveRecord::Migrator.migrations_paths += engine.paths["db/migrate"].to_a
+ end
+ end
end
namespace :create do
@@ -45,8 +52,8 @@ namespace :db do
# Create the SQLite database
ActiveRecord::Base.establish_connection(config)
ActiveRecord::Base.connection
- rescue
- $stderr.puts $!, *($!.backtrace)
+ rescue Exception => e
+ $stderr.puts e, *(e.backtrace)
$stderr.puts "Couldn't create database for #{config.inspect}"
end
end
@@ -61,12 +68,14 @@ namespace :db do
@charset = ENV['CHARSET'] || 'utf8'
@collation = ENV['COLLATION'] || 'utf8_unicode_ci'
creation_options = {:charset => (config['charset'] || @charset), :collation => (config['collation'] || @collation)}
+ error_class = config['adapter'] =~ /mysql2/ ? Mysql2::Error : Mysql::Error
+ access_denied_error = 1045
begin
ActiveRecord::Base.establish_connection(config.merge('database' => nil))
ActiveRecord::Base.connection.create_database(config['database'], creation_options)
ActiveRecord::Base.establish_connection(config)
- rescue Mysql::Error => sqlerr
- if sqlerr.errno == Mysql::Error::ER_ACCESS_DENIED_ERROR
+ rescue error_class => sqlerr
+ if sqlerr.errno == access_denied_error
print "#{sqlerr.error}. \nPlease provide the root password for your mysql installation\n>"
root_password = $stdin.gets.strip
grant_statement = "GRANT ALL PRIVILEGES ON #{config['database']}.* " \
@@ -84,13 +93,13 @@ namespace :db do
end
end
when 'postgresql'
- @encoding = config[:encoding] || ENV['CHARSET'] || 'utf8'
+ @encoding = config['encoding'] || ENV['CHARSET'] || 'utf8'
begin
ActiveRecord::Base.establish_connection(config.merge('database' => 'postgres', 'schema_search_path' => 'public'))
ActiveRecord::Base.connection.create_database(config['database'], config.merge('encoding' => @encoding))
ActiveRecord::Base.establish_connection(config)
- rescue
- $stderr.puts $!, *($!.backtrace)
+ rescue Exception => e
+ $stderr.puts e, *(e.backtrace)
$stderr.puts "Couldn't create database for #{config.inspect}"
end
end
@@ -135,21 +144,21 @@ namespace :db do
desc "Migrate the database (options: VERSION=x, VERBOSE=false)."
- task :migrate => :environment do
+ task :migrate => [:environment, :load_config] do
ActiveRecord::Migration.verbose = ENV["VERBOSE"] ? ENV["VERBOSE"] == "true" : true
- ActiveRecord::Migrator.migrate("db/migrate/", ENV["VERSION"] ? ENV["VERSION"].to_i : nil)
- Rake::Task["db:schema:dump"].invoke if ActiveRecord::Base.schema_format == :ruby
+ ActiveRecord::Migrator.migrate(ActiveRecord::Migrator.migrations_paths, ENV["VERSION"] ? ENV["VERSION"].to_i : nil)
+ db_namespace["schema:dump"].invoke if ActiveRecord::Base.schema_format == :ruby
end
namespace :migrate do
# desc 'Rollbacks the database one migration and re migrate up (options: STEP=x, VERSION=x).'
- task :redo => :environment do
+ task :redo => [:environment, :load_config] do
if ENV["VERSION"]
- Rake::Task["db:migrate:down"].invoke
- Rake::Task["db:migrate:up"].invoke
+ db_namespace["migrate:down"].invoke
+ db_namespace["migrate:up"].invoke
else
- Rake::Task["db:rollback"].invoke
- Rake::Task["db:migrate"].invoke
+ db_namespace["rollback"].invoke
+ db_namespace["migrate"].invoke
end
end
@@ -157,23 +166,23 @@ namespace :db do
task :reset => ["db:drop", "db:create", "db:migrate"]
# desc 'Runs the "up" for a given migration VERSION.'
- task :up => :environment do
+ task :up => [:environment, :load_config] do
version = ENV["VERSION"] ? ENV["VERSION"].to_i : nil
raise "VERSION is required" unless version
- ActiveRecord::Migrator.run(:up, "db/migrate/", version)
- Rake::Task["db:schema:dump"].invoke if ActiveRecord::Base.schema_format == :ruby
+ ActiveRecord::Migrator.run(:up, ActiveRecord::Migrator.migrations_paths, version)
+ db_namespace["schema:dump"].invoke if ActiveRecord::Base.schema_format == :ruby
end
# desc 'Runs the "down" for a given migration VERSION.'
- task :down => :environment do
+ task :down => [:environment, :load_config] do
version = ENV["VERSION"] ? ENV["VERSION"].to_i : nil
raise "VERSION is required" unless version
- ActiveRecord::Migrator.run(:down, "db/migrate/", version)
- Rake::Task["db:schema:dump"].invoke if ActiveRecord::Base.schema_format == :ruby
+ ActiveRecord::Migrator.run(:down, ActiveRecord::Migrator.migrations_paths, version)
+ db_namespace["schema:dump"].invoke if ActiveRecord::Base.schema_format == :ruby
end
desc "Display status of migrations"
- task :status => :environment do
+ task :status => [:environment, :load_config] do
config = ActiveRecord::Base.configurations[Rails.env || 'development']
ActiveRecord::Base.establish_connection(config)
unless ActiveRecord::Base.connection.table_exists?(ActiveRecord::Migrator.schema_migrations_table_name)
@@ -184,7 +193,7 @@ namespace :db do
file_list = []
Dir.foreach(File.join(Rails.root, 'db', 'migrate')) do |file|
# only files matching "20091231235959_some_name.rb" pattern
- if match_data = /(\d{14})_(.+)\.rb/.match(file)
+ if match_data = /^(\d{14})_(.+)\.rb$/.match(file)
status = db_list.delete(match_data[1]) ? 'up' : 'down'
file_list << [status, match_data[1], match_data[2]]
end
@@ -204,17 +213,17 @@ namespace :db do
end
desc 'Rolls the schema back to the previous version (specify steps w/ STEP=n).'
- task :rollback => :environment do
+ task :rollback => [:environment, :load_config] do
step = ENV['STEP'] ? ENV['STEP'].to_i : 1
- ActiveRecord::Migrator.rollback('db/migrate/', step)
- Rake::Task["db:schema:dump"].invoke if ActiveRecord::Base.schema_format == :ruby
+ ActiveRecord::Migrator.rollback(ActiveRecord::Migrator.migrations_paths, step)
+ db_namespace["schema:dump"].invoke if ActiveRecord::Base.schema_format == :ruby
end
# desc 'Pushes the schema to the next version (specify steps w/ STEP=n).'
- task :forward => :environment do
+ task :forward => [:environment, :load_config] do
step = ENV['STEP'] ? ENV['STEP'].to_i : 1
- ActiveRecord::Migrator.forward('db/migrate/', step)
- Rake::Task["db:schema:dump"].invoke if ActiveRecord::Base.schema_format == :ruby
+ ActiveRecord::Migrator.forward(ActiveRecord::Migrator.migrations_paths, step)
+ db_namespace["schema:dump"].invoke if ActiveRecord::Base.schema_format == :ruby
end
# desc 'Drops and recreates the database from db/schema.rb for the current environment and loads the seeds.'
@@ -258,7 +267,7 @@ namespace :db do
# desc "Raises an error if there are pending migrations"
task :abort_if_pending_migrations => :environment do
if defined? ActiveRecord
- pending_migrations = ActiveRecord::Migrator.new(:up, 'db/migrate').pending_migrations
+ pending_migrations = ActiveRecord::Migrator.new(:up, ActiveRecord::Migrator.migrations_paths).pending_migrations
if pending_migrations.any?
puts "You have #{pending_migrations.size} pending migrations:"
@@ -275,8 +284,7 @@ namespace :db do
desc 'Load the seed data from db/seeds.rb'
task :seed => 'db:abort_if_pending_migrations' do
- seed_file = File.join(Rails.root, 'db', 'seeds.rb')
- load(seed_file) if File.exist?(seed_file)
+ Rails.application.load_seed
end
namespace :fixtures do
@@ -288,8 +296,8 @@ namespace :db do
base_dir = ENV['FIXTURES_PATH'] ? File.join(Rails.root, ENV['FIXTURES_PATH']) : File.join(Rails.root, 'test', 'fixtures')
fixtures_dir = ENV['FIXTURES_DIR'] ? File.join(base_dir, ENV['FIXTURES_DIR']) : base_dir
- (ENV['FIXTURES'] ? ENV['FIXTURES'].split(/,/).map {|f| File.join(fixtures_dir, f) } : Dir["#{fixtures_dir}/**/*.{yml,csv}"]).each do |fixture_file|
- Fixtures.create_fixtures(fixtures_dir, fixture_file[(fixtures_dir.size + 1)..-5])
+ (ENV['FIXTURES'] ? ENV['FIXTURES'].split(/,/) : Dir["#{fixtures_dir}/**/*.{yml,csv}"].map {|f| f[(fixtures_dir.size + 1)..-5] }).each do |fixture_file|
+ Fixtures.create_fixtures(fixtures_dir, fixture_file)
end
end
@@ -319,12 +327,12 @@ namespace :db do
namespace :schema do
desc "Create a db/schema.rb file that can be portably used against any DB supported by AR"
- task :dump => :environment do
+ task :dump => :load_config do
require 'active_record/schema_dumper'
File.open(ENV['SCHEMA'] || "#{Rails.root}/db/schema.rb", "w") do |file|
ActiveRecord::SchemaDumper.dump(ActiveRecord::Base.connection, file)
end
- Rake::Task["db:schema:dump"].reenable
+ db_namespace["schema:dump"].reenable
end
desc "Load a schema.rb file into the database"
@@ -333,7 +341,7 @@ namespace :db do
if File.exists?(file)
load(file)
else
- abort %{#{file} doesn't exist yet. Run "rake db:migrate" to create it then try again. If you do not intend to use a database, you should instead alter #{Rails.root}/config/boot.rb to limit the frameworks that will be loaded}
+ abort %{#{file} doesn't exist yet. Run "rake db:migrate" to create it then try again. If you do not intend to use a database, you should instead alter #{Rails.root}/config/application.rb to limit the frameworks that will be loaded}
end
end
end
@@ -367,7 +375,7 @@ namespace :db do
db_string = firebird_db_string(abcs[Rails.env])
sh "isql -a #{db_string} > #{Rails.root}/db/#{Rails.env}_structure.sql"
else
- raise "Task not supported by '#{abcs["test"]["adapter"]}'"
+ raise "Task not supported by '#{abcs[Rails.env]["adapter"]}'"
end
if ActiveRecord::Base.connection.supports_migrations?
@@ -381,7 +389,7 @@ namespace :db do
task :load => 'db:test:purge' do
ActiveRecord::Base.establish_connection(ActiveRecord::Base.configurations['test'])
ActiveRecord::Schema.verbose = false
- Rake::Task["db:schema:load"].invoke
+ db_namespace["schema:load"].invoke
end
# desc "Recreate the test database from the current environment's database schema"
@@ -401,7 +409,7 @@ namespace :db do
ENV['PGHOST'] = abcs["test"]["host"] if abcs["test"]["host"]
ENV['PGPORT'] = abcs["test"]["port"].to_s if abcs["test"]["port"]
ENV['PGPASSWORD'] = abcs["test"]["password"].to_s if abcs["test"]["password"]
- `psql -U "#{abcs["test"]["username"]}" -f #{Rails.root}/db/#{Rails.env}_structure.sql #{abcs["test"]["database"]}`
+ `psql -U "#{abcs["test"]["username"]}" -f #{Rails.root}/db/#{Rails.env}_structure.sql #{abcs["test"]["database"]} #{abcs["test"]["template"]}`
when "sqlite", "sqlite3"
dbfile = abcs["test"]["database"] || abcs["test"]["dbfile"]
`#{abcs["test"]["adapter"]} #{dbfile} < #{Rails.root}/db/#{Rails.env}_structure.sql`
@@ -455,7 +463,7 @@ namespace :db do
# desc 'Check for pending migrations and load the test schema'
task :prepare => 'db:abort_if_pending_migrations' do
if defined?(ActiveRecord) && !ActiveRecord::Base.configurations.blank?
- Rake::Task[{ :sql => "db:test:clone_structure", :ruby => "db:test:load" }[ActiveRecord::Base.schema_format]].invoke
+ db_namespace[{ :sql => "test:clone_structure", :ruby => "test:load" }[ActiveRecord::Base.schema_format]].invoke
end
end
end
@@ -477,6 +485,34 @@ namespace :db do
end
end
+namespace :railties do
+ namespace :install do
+ # desc "Copies missing migrations from Railties (e.g. plugins, engines). You can specify Railties to use with FROM=railtie1,railtie2"
+ task :migrations => :"db:load_config" do
+ to_load = ENV["FROM"].blank? ? :all : ENV["FROM"].split(",").map {|n| n.strip }
+ railties = {}
+ Rails.application.railties.all do |railtie|
+ next unless to_load == :all || to_load.include?(railtie.railtie_name)
+
+ if railtie.respond_to?(:paths) && (path = railtie.paths["db/migrate"].first)
+ railties[railtie.railtie_name] = path
+ end
+ end
+
+ on_skip = Proc.new do |name, migration|
+ puts "NOTE: Migration #{migration.basename} from #{name} has been skipped. Migration with the same name already exists."
+ end
+
+ on_copy = Proc.new do |name, migration, old_path|
+ puts "Copied migration #{migration.basename} from #{name}"
+ end
+
+ ActiveRecord::Migration.copy( ActiveRecord::Migrator.migrations_paths.first, railties,
+ :on_skip => on_skip, :on_copy => on_copy)
+ end
+ end
+end
+
task 'test:prepare' => 'db:test:prepare'
def drop_database(config)
diff --git a/activerecord/lib/active_record/reflection.rb b/activerecord/lib/active_record/reflection.rb
index 7f47a812eb..e801bc4afa 100644
--- a/activerecord/lib/active_record/reflection.rb
+++ b/activerecord/lib/active_record/reflection.rb
@@ -1,10 +1,18 @@
+require 'active_support/core_ext/class/attribute'
+require 'active_support/core_ext/module/deprecation'
+
module ActiveRecord
# = Active Record Reflection
module Reflection # :nodoc:
extend ActiveSupport::Concern
+ included do
+ class_attribute :reflections
+ self.reflections = {}
+ end
+
# Reflection enables to interrogate Active Record classes and objects
- # about their associations and aggregations. This information can,
+ # about their associations and aggregations. This information can,
# for example, be used in a form builder that takes an Active Record object
# and creates input fields for all of the attributes depending on their type
# and displays the associations to other objects.
@@ -20,36 +28,27 @@ module ActiveRecord
when :composed_of
reflection = AggregateReflection.new(macro, name, options, active_record)
end
- write_inheritable_hash :reflections, name => reflection
- reflection
- end
- # Returns a hash containing all AssociationReflection objects for the current class.
- # Example:
- #
- # Invoice.reflections
- # Account.reflections
- #
- def reflections
- read_inheritable_attribute(:reflections) || write_inheritable_attribute(:reflections, {})
+ self.reflections = self.reflections.merge(name => reflection)
+ reflection
end
# Returns an array of AggregateReflection objects for all the aggregations in the class.
def reflect_on_all_aggregations
- reflections.values.select { |reflection| reflection.is_a?(AggregateReflection) }
+ reflections.values.grep(AggregateReflection)
end
- # Returns the AggregateReflection object for the named +aggregation+ (use the symbol).
+ # Returns the AggregateReflection object for the named +aggregation+ (use the symbol).
#
- # Account.reflect_on_aggregation(:balance) #=> the balance AggregateReflection
+ # Account.reflect_on_aggregation(:balance) # => the balance AggregateReflection
#
def reflect_on_aggregation(aggregation)
reflections[aggregation].is_a?(AggregateReflection) ? reflections[aggregation] : nil
end
- # Returns an array of AssociationReflection objects for all the
- # associations in the class. If you only want to reflect on a certain
- # association type, pass in the symbol (<tt>:has_many</tt>, <tt>:has_one</tt>,
+ # Returns an array of AssociationReflection objects for all the
+ # associations in the class. If you only want to reflect on a certain
+ # association type, pass in the symbol (<tt>:has_many</tt>, <tt>:has_one</tt>,
# <tt>:belongs_to</tt>) as the first parameter.
#
# Example:
@@ -58,7 +57,7 @@ module ActiveRecord
# Account.reflect_on_all_associations(:has_many) # returns an array of all has_many associations
#
def reflect_on_all_associations(macro = nil)
- association_reflections = reflections.values.select { |reflection| reflection.is_a?(AssociationReflection) }
+ association_reflections = reflections.values.grep(AssociationReflection)
macro ? association_reflections.select { |reflection| reflection.macro == macro } : association_reflections
end
@@ -78,7 +77,7 @@ module ActiveRecord
end
- # Abstract base class for AggregateReflection and AssociationReflection. Objects of
+ # Abstract base class for AggregateReflection and AssociationReflection. Objects of
# AggregateReflection and AssociationReflection are returned by the Reflection::ClassMethods.
class MacroReflection
attr_reader :active_record
@@ -89,23 +88,23 @@ module ActiveRecord
# Returns the name of the macro.
#
- # <tt>composed_of :balance, :class_name => 'Money'</tt> returns <tt>:balance</tt>
+ # <tt>composed_of :balance, :class_name => 'Money'</tt> returns <tt>:balance</tt>
# <tt>has_many :clients</tt> returns <tt>:clients</tt>
attr_reader :name
- # Returns the macro type.
+ # Returns the macro type.
#
# <tt>composed_of :balance, :class_name => 'Money'</tt> returns <tt>:composed_of</tt>
# <tt>has_many :clients</tt> returns <tt>:has_many</tt>
attr_reader :macro
- # Returns the hash of options used for the macro.
+ # Returns the hash of options used for the macro.
#
# <tt>composed_of :balance, :class_name => 'Money'</tt> returns <tt>{ :class_name => "Money" }</tt>
# <tt>has_many :clients</tt> returns +{}+
attr_reader :options
- # Returns the class for the macro.
+ # Returns the class for the macro.
#
# <tt>composed_of :balance, :class_name => 'Money'</tt> returns the Money class
# <tt>has_many :clients</tt> returns the Client class
@@ -113,7 +112,7 @@ module ActiveRecord
@klass ||= class_name.constantize
end
- # Returns the class name for the macro.
+ # Returns the class name for the macro.
#
# <tt>composed_of :balance, :class_name => 'Money'</tt> returns <tt>'Money'</tt>
# <tt>has_many :clients</tt> returns <tt>'Client'</tt>
@@ -138,12 +137,12 @@ module ActiveRecord
end
- # Holds all the meta-data about an aggregation as it was specified in the
+ # Holds all the meta-data about an aggregation as it was specified in the
# Active Record class.
class AggregateReflection < MacroReflection #:nodoc:
end
- # Holds all the meta-data about an association as it was specified in the
+ # Holds all the meta-data about an association as it was specified in the
# Active Record class.
class AssociationReflection < MacroReflection #:nodoc:
# Returns the target association's class.
@@ -198,8 +197,21 @@ module ActiveRecord
@quoted_table_name ||= klass.quoted_table_name
end
+ def foreign_key
+ @foreign_key ||= options[:foreign_key] || derive_foreign_key
+ end
+
def primary_key_name
- @primary_key_name ||= options[:foreign_key] || derive_primary_key_name
+ foreign_key
+ end
+ deprecate :primary_key_name => :foreign_key
+
+ def foreign_type
+ @foreign_type ||= options[:foreign_type] || "#{name}_type"
+ end
+
+ def type
+ @type ||= "#{options[:as]}_type"
end
def primary_key_column
@@ -207,7 +219,18 @@ module ActiveRecord
end
def association_foreign_key
- @association_foreign_key ||= @options[:association_foreign_key] || class_name.foreign_key
+ @association_foreign_key ||= options[:association_foreign_key] || class_name.foreign_key
+ end
+
+ def association_primary_key
+ @association_primary_key ||=
+ options[:primary_key] ||
+ !options[:polymorphic] && klass.primary_key ||
+ 'id'
+ end
+
+ def active_record_primary_key
+ @active_record_primary_key ||= options[:primary_key] || active_record.primary_key
end
def counter_cache_column
@@ -239,18 +262,32 @@ module ActiveRecord
end
def through_reflection
- false
- end
-
- def through_reflection_primary_key_name
+ nil
end
def source_reflection
nil
end
+ # A chain of reflections from this one back to the owner. For more see the explanation in
+ # ThroughReflection.
+ def chain
+ [self]
+ end
+
+ # An array of arrays of conditions. Each item in the outside array corresponds to a reflection
+ # in the #chain. The inside arrays are simply conditions (and each condition may itself be
+ # a hash, array, arel predicate, etc...)
+ def conditions
+ conditions = [options[:conditions]].compact
+ conditions << { type => active_record.base_class.name } if options[:as]
+ [conditions]
+ end
+
+ alias :source_macro :macro
+
def has_inverse?
- !@options[:inverse_of].nil?
+ @options[:inverse_of]
end
def inverse_of
@@ -289,22 +326,36 @@ module ActiveRecord
!options[:validate].nil? ? options[:validate] : (options[:autosave] == true || macro == :has_many)
end
- def dependent_conditions(record, base_class, extra_conditions)
- dependent_conditions = []
- dependent_conditions << "#{primary_key_name} = #{record.send(name).send(:owner_quoted_id)}"
- dependent_conditions << "#{options[:as]}_type = '#{base_class.name}'" if options[:as]
- dependent_conditions << klass.send(:sanitize_sql, options[:conditions]) if options[:conditions]
- dependent_conditions << extra_conditions if extra_conditions
- dependent_conditions = dependent_conditions.collect {|where| "(#{where})" }.join(" AND ")
- dependent_conditions = dependent_conditions.gsub('@', '\@')
- dependent_conditions
- end
-
# Returns +true+ if +self+ is a +belongs_to+ reflection.
def belongs_to?
macro == :belongs_to
end
+ def association_class
+ case macro
+ when :belongs_to
+ if options[:polymorphic]
+ Associations::BelongsToPolymorphicAssociation
+ else
+ Associations::BelongsToAssociation
+ end
+ when :has_and_belongs_to_many
+ Associations::HasAndBelongsToManyAssociation
+ when :has_many
+ if options[:through]
+ Associations::HasManyThroughAssociation
+ else
+ Associations::HasManyAssociation
+ end
+ when :has_one
+ if options[:through]
+ Associations::HasOneThroughAssociation
+ else
+ Associations::HasOneAssociation
+ end
+ end
+ end
+
private
def derive_class_name
class_name = name.to_s.camelize
@@ -312,7 +363,7 @@ module ActiveRecord
class_name
end
- def derive_primary_key_name
+ def derive_foreign_key
if belongs_to?
"#{name}_id"
elsif options[:as]
@@ -326,6 +377,8 @@ module ActiveRecord
# Holds all the meta-data about a :through association as it was specified
# in the Active Record class.
class ThroughReflection < AssociationReflection #:nodoc:
+ delegate :foreign_key, :foreign_type, :association_foreign_key, :active_record_primary_key, :to => :source_reflection
+
# Gets the source of the through reflection. It checks both a singularized
# and pluralized form for <tt>:belongs_to</tt> or <tt>:has_many</tt>.
#
@@ -353,6 +406,88 @@ module ActiveRecord
@through_reflection ||= active_record.reflect_on_association(options[:through])
end
+ # Returns an array of reflections which are involved in this association. Each item in the
+ # array corresponds to a table which will be part of the query for this association.
+ #
+ # The chain is built by recursively calling #chain on the source reflection and the through
+ # reflection. The base case for the recursion is a normal association, which just returns
+ # [self] as its #chain.
+ def chain
+ @chain ||= begin
+ chain = source_reflection.chain + through_reflection.chain
+ chain[0] = self # Use self so we don't lose the information from :source_type
+ chain
+ end
+ end
+
+ # Consider the following example:
+ #
+ # class Person
+ # has_many :articles
+ # has_many :comment_tags, :through => :articles
+ # end
+ #
+ # class Article
+ # has_many :comments
+ # has_many :comment_tags, :through => :comments, :source => :tags
+ # end
+ #
+ # class Comment
+ # has_many :tags
+ # end
+ #
+ # There may be conditions on Person.comment_tags, Article.comment_tags and/or Comment.tags,
+ # but only Comment.tags will be represented in the #chain. So this method creates an array
+ # of conditions corresponding to the chain. Each item in the #conditions array corresponds
+ # to an item in the #chain, and is itself an array of conditions from an arbitrary number
+ # of relevant reflections, plus any :source_type or polymorphic :as constraints.
+ def conditions
+ @conditions ||= begin
+ conditions = source_reflection.conditions
+
+ # Add to it the conditions from this reflection if necessary.
+ conditions.first << options[:conditions] if options[:conditions]
+
+ through_conditions = through_reflection.conditions
+
+ if options[:source_type]
+ through_conditions.first << { foreign_type => options[:source_type] }
+ end
+
+ # Recursively fill out the rest of the array from the through reflection
+ conditions += through_conditions
+
+ # And return
+ conditions
+ end
+ end
+
+ # The macro used by the source association
+ def source_macro
+ source_reflection.source_macro
+ end
+
+ # A through association is nested iff there would be more than one join table
+ def nested?
+ chain.length > 2 || through_reflection.macro == :has_and_belongs_to_many
+ end
+
+ # We want to use the klass from this reflection, rather than just delegate straight to
+ # the source_reflection, because the source_reflection may be polymorphic. We still
+ # need to respect the source_reflection's :primary_key option, though.
+ def association_primary_key
+ @association_primary_key ||= begin
+ # Get the "actual" source reflection if the immediate source reflection has a
+ # source reflection itself
+ source_reflection = self.source_reflection
+ while source_reflection.source_reflection
+ source_reflection = source_reflection.source_reflection
+ end
+
+ source_reflection.options[:primary_key] || klass.primary_key
+ end
+ end
+
# Gets an array of possible <tt>:through</tt> source reflection names:
#
# [:singularized, :pluralized]
@@ -361,11 +496,23 @@ module ActiveRecord
@source_reflection_names ||= (options[:source] ? [options[:source]] : [name.to_s.singularize, name]).collect { |n| n.to_sym }
end
+ def source_options
+ source_reflection.options
+ end
+
+ def through_options
+ through_reflection.options
+ end
+
def check_validity!
if through_reflection.nil?
raise HasManyThroughAssociationNotFoundError.new(active_record.name, self)
end
+ if through_reflection.options[:polymorphic]
+ raise HasManyThroughAssociationPolymorphicThroughError.new(active_record.name, self)
+ end
+
if source_reflection.nil?
raise HasManyThroughSourceAssociationNotFoundError.new(self)
end
@@ -375,24 +522,16 @@ module ActiveRecord
end
if source_reflection.options[:polymorphic] && options[:source_type].nil?
- raise HasManyThroughAssociationPolymorphicError.new(active_record.name, self, source_reflection)
+ raise HasManyThroughAssociationPolymorphicSourceError.new(active_record.name, self, source_reflection)
end
- unless [:belongs_to, :has_many, :has_one].include?(source_reflection.macro) && source_reflection.options[:through].nil?
- raise HasManyThroughSourceAssociationMacroError.new(self)
+ if macro == :has_one && through_reflection.collection?
+ raise HasOneThroughCantAssociateThroughCollection.new(active_record.name, self, through_reflection)
end
check_validity_of_inverse!
end
- def through_reflection_primary_key
- through_reflection.belongs_to? ? through_reflection.klass.primary_key : through_reflection.primary_key_name
- end
-
- def through_reflection_primary_key_name
- through_reflection.primary_key_name if through_reflection.belongs_to?
- end
-
private
def derive_class_name
# get the class_name of the belongs_to association of the through reflection
diff --git a/activerecord/lib/active_record/relation.rb b/activerecord/lib/active_record/relation.rb
index 30be723291..8e545f9cad 100644
--- a/activerecord/lib/active_record/relation.rb
+++ b/activerecord/lib/active_record/relation.rb
@@ -5,13 +5,14 @@ module ActiveRecord
class Relation
JoinOperation = Struct.new(:relation, :join_class, :on)
ASSOCIATION_METHODS = [:includes, :eager_load, :preload]
- MULTI_VALUE_METHODS = [:select, :group, :order, :joins, :where, :having]
+ MULTI_VALUE_METHODS = [:select, :group, :order, :joins, :where, :having, :bind]
SINGLE_VALUE_METHODS = [:limit, :offset, :lock, :readonly, :create_with, :from]
include FinderMethods, Calculations, SpawnMethods, QueryMethods, Batches
+ # These are explicitly delegated to improve performance (avoids method_missing)
delegate :to_xml, :to_yaml, :length, :collect, :map, :each, :all?, :include?, :to => :to_a
- delegate :insert, :to => :arel
+ delegate :table_name, :primary_key, :to => :klass
attr_reader :table, :klass, :loaded
attr_accessor :extensions
@@ -28,6 +29,36 @@ module ActiveRecord
@extensions = []
end
+ def insert(values)
+ primary_key_value = nil
+
+ if primary_key && Hash === values
+ primary_key_value = values[values.keys.find { |k|
+ k.name == primary_key
+ }]
+
+ if !primary_key_value && connection.prefetch_primary_key?(klass.table_name)
+ primary_key_value = connection.next_sequence_value(klass.sequence_name)
+ values[klass.arel_table[klass.primary_key]] = primary_key_value
+ end
+ end
+
+ im = arel.create_insert
+ im.into @table
+
+ if values.empty? # empty insert
+ im.values = im.create_values [connection.null_insert_value], []
+ else
+ im.insert values
+ end
+
+ @klass.connection.insert(
+ im.to_sql,
+ 'SQL',
+ primary_key,
+ primary_key_value)
+ end
+
def new(*args, &block)
scoping { @klass.new(*args, &block) }
end
@@ -47,27 +78,30 @@ module ActiveRecord
end
def respond_to?(method, include_private = false)
- return true if arel.respond_to?(method, include_private) || Array.method_defined?(method) || @klass.respond_to?(method, include_private)
-
- if match = DynamicFinderMatch.match(method)
- return true if @klass.send(:all_attributes_exists?, match.attribute_names)
- elsif match = DynamicScopeMatch.match(method)
- return true if @klass.send(:all_attributes_exists?, match.attribute_names)
- else
+ arel.respond_to?(method, include_private) ||
+ Array.method_defined?(method) ||
+ @klass.respond_to?(method, include_private) ||
super
- end
end
def to_a
return @records if loaded?
- @records = eager_loading? ? find_with_associations : @klass.find_by_sql(arel.to_sql)
+ @records = if @readonly_value.nil? && !@klass.locking_enabled?
+ eager_loading? ? find_with_associations : @klass.find_by_sql(arel.to_sql, @bind_values)
+ else
+ IdentityMap.without do
+ eager_loading? ? find_with_associations : @klass.find_by_sql(arel.to_sql, @bind_values)
+ end
+ end
preload = @preload_values
preload += @includes_values unless eager_loading?
- preload.each {|associations| @klass.send(:preload_associations, @records, associations) }
+ preload.each do |associations|
+ ActiveRecord::Associations::Preloader.new(@records, associations).run
+ end
- # @readonly_value is true only if set explicitly. @implicit_readonly is true if there
+ # @readonly_value is true only if set explicitly. @implicit_readonly is true if there
# are JOINS and no explicit SELECT.
readonly = @readonly_value.nil? ? @implicit_readonly : @readonly_value
@records.each { |record| record.readonly! } if readonly
@@ -76,7 +110,9 @@ module ActiveRecord
@records
end
- def as_json(options = nil) to_a end #:nodoc:
+ def as_json(options = nil) #:nodoc:
+ to_a.as_json(options)
+ end
# Returns size of the records.
def size
@@ -85,7 +121,10 @@ module ActiveRecord
# Returns true if there are no records.
def empty?
- loaded? ? @records.empty? : count.zero?
+ return @records.empty? if loaded?
+
+ c = count
+ c.respond_to?(:zero?) ? c.zero? : c.empty?
end
def any?
@@ -131,7 +170,7 @@ module ActiveRecord
# ==== Parameters
#
# * +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.
+ # * +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.
#
@@ -148,16 +187,29 @@ module ActiveRecord
#
# # Update all books that match conditions, but limit it to 5 ordered by date
# Book.update_all "author = 'David'", "title LIKE '%Rails%'", :order => 'created_at', :limit => 5
+ #
+ # # Conditions from the current relation also works
+ # Book.where('title LIKE ?', '%Rails%').update_all(:author => 'David')
+ #
+ # # The same idea applies to limit and order
+ # Book.where('title LIKE ?', '%Rails%').order(:created_at).limit(5).update_all(:author => 'David')
def update_all(updates, conditions = nil, options = {})
if conditions || options.present?
where(conditions).apply_finder_options(options.slice(:limit, :order)).update_all(updates)
else
+ limit = nil
+ order = []
# Apply limit and order only if they're both present
if @limit_value.present? == @order_values.present?
- arel.update(@klass.send(:sanitize_sql_for_assignment, updates))
- else
- except(:limit, :order).update_all(updates)
+ limit = arel.limit
+ order = arel.orders
end
+
+ stmt = arel.compile_update(Arel.sql(@klass.send(:sanitize_sql_for_assignment, updates)))
+ stmt.take limit if limit
+ stmt.order(*order)
+ stmt.key = table[primary_key]
+ @klass.connection.update stmt.to_sql
end
end
@@ -214,6 +266,7 @@ module ActiveRecord
#
# Person.destroy_all("last_login < '2004-04-04'")
# Person.destroy_all(:status => "inactive")
+ # Person.where(:age => 0..18).destroy_all
def destroy_all(conditions = nil)
if conditions
where(conditions).destroy_all
@@ -263,12 +316,20 @@ module ActiveRecord
#
# Post.delete_all("person_id = 5 AND (category = 'Something' OR category = 'Else')")
# Post.delete_all(["person_id = ? AND (category = ? OR category = ?)", 5, 'Something', 'Else'])
+ # Post.where(:person_id => 5).where(:category => ['Something', 'Else']).delete_all
#
- # Both calls delete the affected posts all at once with a single DELETE statement.
- # If you need to destroy dependent associations or call your <tt>before_*</tt> or
+ # Both calls delete the affected posts all at once with a single DELETE statement.
+ # If you need to destroy dependent associations or call your <tt>before_*</tt> or
# +after_destroy+ callbacks, use the +destroy_all+ method instead.
def delete_all(conditions = nil)
- conditions ? where(conditions).delete_all : arel.delete.tap { reset }
+ if conditions
+ where(conditions).delete_all
+ else
+ statement = arel.compile_delete
+ affected = @klass.connection.delete statement.to_sql
+ reset
+ affected
+ end
end
# Deletes the row with a primary key matching the +id+ argument, using a
@@ -292,7 +353,7 @@ module ActiveRecord
# # Delete multiple rows
# Todo.delete([2,3,4])
def delete(id_or_array)
- where(@klass.primary_key => id_or_array).delete_all
+ where(primary_key => id_or_array).delete_all
end
def reload
@@ -308,25 +369,20 @@ module ActiveRecord
self
end
- def primary_key
- @primary_key ||= table[@klass.primary_key]
- end
-
def to_sql
@to_sql ||= arel.to_sql
end
+ def where_values_hash
+ equalities = @where_values.grep(Arel::Nodes::Equality).find_all { |node|
+ node.left.relation.name == table_name
+ }
+
+ Hash[equalities.map { |where| [where.left.name, where.right] }]
+ end
+
def scope_for_create
- @scope_for_create ||= begin
- @create_with_value || Hash[
- @where_values.find_all { |w|
- w.respond_to?(:operator) && w.operator == :==
- }.map { |where|
- [where.operand1.name,
- where.operand2.respond_to?(:value) ?
- where.operand2.value : where.operand2]
- }]
- end
+ @scope_for_create ||= where_values_hash.merge(@create_with_value || {})
end
def eager_loading?
@@ -338,7 +394,7 @@ module ActiveRecord
when Relation
other.to_sql == to_sql
when Array
- to_a == other.to_a
+ to_a == other
end
end
@@ -357,15 +413,6 @@ module ActiveRecord
scoping { @klass.send(method, *args, &block) }
elsif arel.respond_to?(method)
arel.send(method, *args, &block)
- elsif match = DynamicFinderMatch.match(method)
- attributes = match.attribute_names
- super unless @klass.send(:all_attributes_exists?, attributes)
-
- if match.finder?
- find_by_attributes(match, attributes, *args)
- elsif match.instantiator?
- find_or_instantiator_by_attributes(match, attributes, *args, &block)
- end
else
super
end
@@ -374,8 +421,19 @@ module ActiveRecord
private
def references_eager_loaded_tables?
+ joined_tables = arel.join_sources.map do |join|
+ if join.is_a?(Arel::Nodes::StringJoin)
+ tables_in_string(join.left)
+ else
+ [join.left.table_name, join.left.table_alias]
+ end
+ end
+
+ joined_tables += [table.name, table.table_alias]
+
# always convert table names to downcase as in Oracle quoted table names are in uppercase
- joined_tables = (tables_in_string(arel.joins(arel)) + [table.name, table.table_alias]).compact.map(&:downcase).uniq
+ joined_tables = joined_tables.flatten.compact.map { |t| t.downcase }.uniq
+
(tables_in_string(to_sql) - joined_tables).any?
end
@@ -383,7 +441,7 @@ module ActiveRecord
return [] if string.blank?
# always convert table names to downcase as in Oracle quoted table names are in uppercase
# ignore raw_sql_ that is used by Oracle adapter as alias for limit/offset subqueries
- string.scan(/([a-zA-Z_][\.\w]+).?\./).flatten.map(&:downcase).uniq - ['raw_sql_']
+ string.scan(/([a-zA-Z_][.\w]+).?\./).flatten.map{ |s| s.downcase }.uniq - ['raw_sql_']
end
end
diff --git a/activerecord/lib/active_record/relation/batches.rb b/activerecord/lib/active_record/relation/batches.rb
index d7494ebb5a..bf5a60f458 100644
--- a/activerecord/lib/active_record/relation/batches.rb
+++ b/activerecord/lib/active_record/relation/batches.rb
@@ -1,7 +1,7 @@
require 'active_support/core_ext/object/blank'
module ActiveRecord
- module Batches # :nodoc:
+ module Batches
# 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).
@@ -39,7 +39,7 @@ module ActiveRecord
# 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.
+ # the batch sizes.
#
# Example:
#
@@ -50,7 +50,7 @@ module ActiveRecord
def find_in_batches(options = {})
relation = self
- if orders.present? || taken.present?
+ unless arel.orders.blank? && arel.taken.blank?
ActiveRecord::Base.logger.warn("Scoped order and limit are ignored, it's forced to be batch order and batch size")
end
@@ -65,7 +65,7 @@ module ActiveRecord
batch_size = options.delete(:batch_size) || 1000
relation = relation.except(:order).order(batch_order).limit(batch_size)
- records = relation.where(primary_key.gteq(start)).all
+ records = relation.where(table[primary_key].gteq(start)).all
while records.any?
yield records
@@ -73,7 +73,7 @@ module ActiveRecord
break if records.size < batch_size
if primary_key_offset = records.last.id
- records = relation.where(primary_key.gt(primary_key_offset)).to_a
+ records = relation.where(table[primary_key].gt(primary_key_offset)).to_a
else
raise "Primary key not included in the custom select clause"
end
@@ -83,7 +83,7 @@ module ActiveRecord
private
def batch_order
- "#{@klass.table_name}.#{@klass.primary_key} ASC"
+ "#{table_name}.#{primary_key} ASC"
end
end
end
diff --git a/activerecord/lib/active_record/relation/calculations.rb b/activerecord/lib/active_record/relation/calculations.rb
index a679c444cf..c1842b1a96 100644
--- a/activerecord/lib/active_record/relation/calculations.rb
+++ b/activerecord/lib/active_record/relation/calculations.rb
@@ -6,32 +6,30 @@ module ActiveRecord
# Count operates using three different approaches.
#
# * Count all: By not passing any parameters to count, it will return a count of all the rows for the model.
- # * Count using column: By passing a column name to count, it will return a count of all the
- # rows for the model with supplied column present
+ # * Count using column: By passing a column name to count, it will return a count of all the
+ # rows for the model with supplied column present.
# * Count using options will find the row count matched by the options used.
#
# The third approach, count using options, accepts an option hash as the only parameter. The options are:
#
- # * <tt>:conditions</tt>: An SQL fragment like "administrator = 1" or [ "user_name = ?", username ].
+ # * <tt>:conditions</tt>: An SQL fragment like "administrator = 1" or [ "user_name = ?", username ].
# See conditions in the intro to ActiveRecord::Base.
- # * <tt>:joins</tt>: Either an SQL fragment for additional joins like "LEFT JOIN comments ON comments.post_id = id" (rarely needed)
- # or named associations in the same form used for the <tt>:include</tt> option, which will
- # perform an INNER JOIN on the associated table(s).
- # If the value is a string, then the records will be returned read-only since they will have
- # attributes that do not correspond to the table's columns.
+ # * <tt>:joins</tt>: Either an SQL fragment for additional joins like "LEFT JOIN comments ON comments.post_id = id"
+ # (rarely needed) or named associations in the same form used for the <tt>:include</tt> option, which will
+ # perform an INNER JOIN on the associated table(s). If the value is a string, then the records
+ # will be returned read-only since they will have attributes that do not correspond to the table's columns.
# Pass <tt>:readonly => false</tt> to override.
- # * <tt>:include</tt>: Named associations that should be loaded alongside using LEFT OUTER JOINs.
- # The symbols named refer to already defined associations. When using named associations, count
+ # * <tt>:include</tt>: Named associations that should be loaded alongside using LEFT OUTER JOINs.
+ # The symbols named refer to already defined associations. When using named associations, count
# returns the number of DISTINCT items for the model you're counting.
# See eager loading under Associations.
# * <tt>:order</tt>: An SQL fragment like "created_at DESC, name" (really only used with GROUP BY calculations).
# * <tt>:group</tt>: An attribute name by which the result should be grouped. Uses the GROUP BY SQL-clause.
- # * <tt>:select</tt>: By default, this is * as in SELECT * FROM, but can be changed if you, for example,
- # want to do a join but not
- # include the joined columns.
- # * <tt>:distinct</tt>: Set this to true to make this a distinct calculation, such as
+ # * <tt>:select</tt>: By default, this is * as in SELECT * FROM, but can be changed if you, for example,
+ # want to do a join but not include the joined columns.
+ # * <tt>:distinct</tt>: Set this to true to make this a distinct calculation, such as
# SELECT COUNT(DISTINCT posts.id) ...
- # * <tt>:from</tt> - By default, this is the table name of the class, but can be changed to an
+ # * <tt>:from</tt> - By default, this is the table name of the class, but can be changed to an
# alternate table name (or even the name of a database view).
#
# Examples for counting all:
@@ -44,16 +42,16 @@ module ActiveRecord
# Person.count(:conditions => "age > 26")
#
# # because of the named association, it finds the DISTINCT count using LEFT OUTER JOIN.
- # Person.count(:conditions => "age > 26 AND job.salary > 60000", :include => :job)
+ # Person.count(:conditions => "age > 26 AND job.salary > 60000", :include => :job)
#
# # finds the number of rows matching the conditions and joins.
- # Person.count(:conditions => "age > 26 AND job.salary > 60000",
- # :joins => "LEFT JOIN jobs on jobs.person_id = person.id")
+ # Person.count(:conditions => "age > 26 AND job.salary > 60000",
+ # :joins => "LEFT JOIN jobs on jobs.person_id = person.id")
#
# Person.count('id', :conditions => "age > 26") # Performs a COUNT(id)
# Person.count(:all, :conditions => "age > 26") # Performs a COUNT(*) (:all is an alias for '*')
#
- # Note: <tt>Person.count(:all)</tt> will not work because it will use <tt>:all</tt> as the condition.
+ # Note: <tt>Person.count(:all)</tt> will not work because it will use <tt>:all</tt> as the condition.
# Use Person.count instead.
def count(column_name = nil, options = {})
column_name, options = nil, column_name if column_name.is_a?(Hash)
@@ -95,14 +93,14 @@ module ActiveRecord
calculate(:sum, column_name, options)
end
- # This calculates aggregate values in the given column. Methods for count, sum, average,
- # minimum, and maximum have been added as shortcuts. Options such as <tt>:conditions</tt>,
+ # This calculates aggregate values in the given column. Methods for count, sum, average,
+ # minimum, and maximum have been added as shortcuts. Options such as <tt>:conditions</tt>,
# <tt>:order</tt>, <tt>:group</tt>, <tt>:having</tt>, and <tt>:joins</tt> can be passed to customize the query.
#
# There are two basic forms of output:
- # * Single aggregate value: The single value is type cast to Fixnum for COUNT, Float
+ # * Single aggregate value: The single value is type cast to Fixnum for COUNT, Float
# for AVG, and the given column's type for everything else.
- # * Grouped values: This returns an ordered hash of the values and groups them by the
+ # * Grouped values: This returns an ordered hash of the values and groups them by the
# <tt>:group</tt> option. It takes either a column name, or the name of a belongs_to association.
#
# values = Person.maximum(:age, :group => 'last_name')
@@ -119,29 +117,29 @@ module ActiveRecord
# end
#
# Options:
- # * <tt>:conditions</tt> - An SQL fragment like "administrator = 1" or [ "user_name = ?", username ].
+ # * <tt>:conditions</tt> - An SQL fragment like "administrator = 1" or [ "user_name = ?", username ].
# See conditions in the intro to ActiveRecord::Base.
- # * <tt>:include</tt>: Eager loading, see Associations for details. Since calculations don't load anything,
+ # * <tt>:include</tt>: Eager loading, see Associations for details. Since calculations don't load anything,
# the purpose of this is to access fields on joined tables in your conditions, order, or group clauses.
- # * <tt>:joins</tt> - An SQL fragment for additional joins like "LEFT JOIN comments ON comments.post_id = id".
+ # * <tt>:joins</tt> - An SQL fragment for additional joins like "LEFT JOIN comments ON comments.post_id = id".
# (Rarely needed).
- # The records will be returned read-only since they will have attributes that do not correspond to the
+ # The records will be returned read-only since they will have attributes that do not correspond to the
# table's columns.
# * <tt>:order</tt> - An SQL fragment like "created_at DESC, name" (really only used with GROUP BY calculations).
# * <tt>:group</tt> - An attribute name by which the result should be grouped. Uses the GROUP BY SQL-clause.
- # * <tt>:select</tt> - By default, this is * as in SELECT * FROM, but can be changed if you for example
+ # * <tt>:select</tt> - By default, this is * as in SELECT * FROM, but can be changed if you for example
# want to do a join, but not include the joined columns.
- # * <tt>:distinct</tt> - Set this to true to make this a distinct calculation, such as
+ # * <tt>:distinct</tt> - Set this to true to make this a distinct calculation, such as
# SELECT COUNT(DISTINCT posts.id) ...
#
# Examples:
# Person.calculate(:count, :all) # The same as Person.count
# Person.average(:age) # SELECT AVG(age) FROM people...
- # Person.minimum(:age, :conditions => ['last_name != ?', 'Drake']) # Selects the minimum age for
+ # Person.minimum(:age, :conditions => ['last_name != ?', 'Drake']) # Selects the minimum age for
# # everyone with a last name other than 'Drake'
#
# # Selects the minimum age for any family without any minors
- # Person.minimum(:age, :having => 'min(age) > 17', :group => :last_name)
+ # Person.minimum(:age, :having => 'min(age) > 17', :group => :last_name)
#
# Person.sum("2 * age")
def calculate(operation, column_name, options = {})
@@ -163,80 +161,112 @@ module ActiveRecord
def perform_calculation(operation, column_name, options = {})
operation = operation.to_s.downcase
+ distinct = nil
+
if operation == "count"
column_name ||= (select_for_count || :all)
- joins = arel.joins(arel)
- if joins.present? && joins =~ /LEFT OUTER/i
+ unless arel.ast.grep(Arel::Nodes::OuterJoin).empty?
distinct = true
- column_name = @klass.primary_key if column_name == :all
+ column_name = primary_key if column_name == :all
end
- distinct = nil if column_name.to_s =~ /\s*DISTINCT\s+/i
- distinct ||= options[:distinct]
- else
- distinct = nil
+ distinct = nil if column_name =~ /\s*DISTINCT\s+/i
end
distinct = options[:distinct] || distinct
- column_name = :all if column_name.blank? && operation == "count"
if @group_values.any?
- return execute_grouped_calculation(operation, column_name)
+ execute_grouped_calculation(operation, column_name, distinct)
else
- return execute_simple_calculation(operation, column_name, distinct)
+ execute_simple_calculation(operation, column_name, distinct)
end
end
- def execute_simple_calculation(operation, column_name, distinct) #:nodoc:
- column = if @klass.column_names.include?(column_name.to_s)
- Arel::Attribute.new(@klass.unscoped, column_name)
+ def aggregate_column(column_name)
+ if @klass.column_names.include?(column_name.to_s)
+ Arel::Attribute.new(@klass.unscoped.table, column_name)
else
- Arel::SqlLiteral.new(column_name == :all ? "*" : column_name.to_s)
+ Arel.sql(column_name == :all ? "*" : column_name.to_s)
end
+ end
- # Postgresql doesn't like ORDER BY when there are no GROUP BY
- relation = except(:order).select(operation == 'count' ? column.count(distinct) : column.send(operation))
- type_cast_calculated_value(@klass.connection.select_value(relation.to_sql), column_for(column_name), operation)
+ def operation_over_aggregate_column(column, operation, distinct)
+ operation == 'count' ? column.count(distinct) : column.send(operation)
end
- def execute_grouped_calculation(operation, column_name) #:nodoc:
- group_attr = @group_values.first
- association = @klass.reflect_on_association(group_attr.to_sym)
- associated = association && association.macro == :belongs_to # only count belongs_to associations
- group_field = associated ? association.primary_key_name : group_attr
- group_alias = column_alias_for(group_field)
- group_column = column_for(group_field)
+ def execute_simple_calculation(operation, column_name, distinct) #:nodoc:
+ column = aggregate_column(column_name)
- group = @klass.connection.adapter_name == 'FrontBase' ? group_alias : group_field
+ # Postgresql doesn't like ORDER BY when there are no GROUP BY
+ relation = except(:order)
+ select_value = operation_over_aggregate_column(column, operation, distinct)
+
+ relation.select_values = [select_value]
- aggregate_alias = column_alias_for(operation, column_name)
+ query_builder = relation.arel
- select_statement = if operation == 'count' && column_name == :all
- "COUNT(*) AS count_all"
+ if operation == "count"
+ limit = relation.limit_value
+ offset = relation.offset_value
+
+ unless limit && offset
+ query_builder.limit = nil
+ query_builder.offset = nil
+ end
+ end
+
+ type_cast_calculated_value(@klass.connection.select_value(query_builder.to_sql), column_for(column_name), operation)
+ end
+
+ def execute_grouped_calculation(operation, column_name, distinct) #:nodoc:
+ group_attr = @group_values
+ association = @klass.reflect_on_association(group_attr.first.to_sym)
+ associated = group_attr.size == 1 && association && association.macro == :belongs_to # only count belongs_to associations
+ group_fields = Array(associated ? association.foreign_key : group_attr)
+ group_aliases = group_fields.map { |field| column_alias_for(field) }
+ group_columns = group_aliases.zip(group_fields).map { |aliaz,field|
+ [aliaz, column_for(field)]
+ }
+
+ group = @klass.connection.adapter_name == 'FrontBase' ? group_aliases : group_fields
+
+ if operation == 'count' && column_name == :all
+ aggregate_alias = 'count_all'
else
- Arel::Attribute.new(@klass.unscoped, column_name).send(operation).as(aggregate_alias).to_sql
+ aggregate_alias = column_alias_for(operation, column_name)
end
- select_statement << ", #{group_field} AS #{group_alias}"
+ select_values = [
+ operation_over_aggregate_column(
+ aggregate_column(column_name),
+ operation,
+ distinct).as(aggregate_alias)
+ ]
+
+ select_values.concat group_fields.zip(group_aliases).map { |field,aliaz|
+ "#{field} AS #{aliaz}"
+ }
- relation = except(:group).select(select_statement).group(group)
+ relation = except(:group).group(group.join(','))
+ relation.select_values = select_values
calculated_data = @klass.connection.select_all(relation.to_sql)
if association
- key_ids = calculated_data.collect { |row| row[group_alias] }
+ key_ids = calculated_data.collect { |row| row[group_aliases.first] }
key_records = association.klass.base_class.find(key_ids)
- key_records = key_records.inject({}) { |hsh, r| hsh.merge(r.id => r) }
+ key_records = Hash[key_records.map { |r| [r.id, r] }]
end
- calculated_data.inject(ActiveSupport::OrderedHash.new) do |all, row|
- key = type_cast_calculated_value(row[group_alias], group_column)
- key = key_records[key] if associated
- value = row[aggregate_alias]
- all[key] = type_cast_calculated_value(value, column_for(column_name), operation)
- all
- end
+ ActiveSupport::OrderedHash[calculated_data.map do |row|
+ key = group_columns.map { |aliaz, column|
+ type_cast_calculated_value(row[aliaz], column)
+ }
+ key = key.first if key.size == 1
+ key = key_records[key] if associated
+ [key, type_cast_calculated_value(row[aggregate_alias], column_for(column_name), operation)]
+ end]
end
# Converts the given keys to the value that the database adapter returns as
@@ -264,15 +294,11 @@ module ActiveRecord
end
def type_cast_calculated_value(value, column, operation = nil)
- if value.is_a?(String) || value.nil?
- case operation
- when 'count' then value.to_i
- when 'sum' then type_cast_using_column(value || '0', column)
- when 'average' then value.try(:to_d)
- else type_cast_using_column(value, column)
- end
- else
- value
+ case operation
+ when 'count' then value.to_i
+ when 'sum' then type_cast_using_column(value || '0', column)
+ when 'average' then value.respond_to?(:to_d) ? value.to_d : value
+ else type_cast_using_column(value, column)
end
end
@@ -282,7 +308,7 @@ module ActiveRecord
def select_for_count
if @select_values.present?
- select = @select_values.join(", ")
+ select = @select_values.join(", ")
select if select !~ /(,|\*)/
end
end
diff --git a/activerecord/lib/active_record/relation/finder_methods.rb b/activerecord/lib/active_record/relation/finder_methods.rb
index b34c11973b..25e23a9d55 100644
--- a/activerecord/lib/active_record/relation/finder_methods.rb
+++ b/activerecord/lib/active_record/relation/finder_methods.rb
@@ -19,29 +19,29 @@ module ActiveRecord
#
# All approaches accept an options hash as their last parameter.
#
- # ==== Parameters
+ # ==== Options
#
- # * <tt>:conditions</tt> - An SQL fragment like "administrator = 1", <tt>[ "user_name = ?", username ]</tt>,
+ # * <tt>:conditions</tt> - An SQL fragment like "administrator = 1", <tt>["user_name = ?", username]</tt>,
# or <tt>["user_name = :user_name", { :user_name => user_name }]</tt>. See conditions in the intro.
# * <tt>:order</tt> - An SQL fragment like "created_at DESC, name".
# * <tt>:group</tt> - An attribute name by which the result should be grouped. Uses the <tt>GROUP BY</tt> SQL-clause.
- # * <tt>:having</tt> - Combined with +:group+ this can be used to filter the records that a
+ # * <tt>:having</tt> - Combined with +:group+ this can be used to filter the records that a
# <tt>GROUP BY</tt> returns. Uses the <tt>HAVING</tt> SQL-clause.
# * <tt>:limit</tt> - An integer determining the limit on the number of rows that should be returned.
- # * <tt>:offset</tt> - An integer determining the offset from where the rows should be fetched. So at 5,
+ # * <tt>:offset</tt> - An integer determining the offset from where the rows should be fetched. So at 5,
# it would skip rows 0 through 4.
# * <tt>:joins</tt> - Either an SQL fragment for additional joins like "LEFT JOIN comments ON comments.post_id = id" (rarely needed),
- # named associations in the same form used for the <tt>:include</tt> option, which will perform an
+ # named associations in the same form used for the <tt>:include</tt> option, which will perform an
# <tt>INNER JOIN</tt> on the associated table(s),
# or an array containing a mixture of both strings and named associations.
- # If the value is a string, then the records will be returned read-only since they will
+ # If the value is a string, then the records will be returned read-only since they will
# have attributes that do not correspond to the table's columns.
# Pass <tt>:readonly => false</tt> to override.
# * <tt>:include</tt> - Names associations that should be loaded alongside. The symbols named refer
# to already defined associations. See eager loading under Associations.
- # * <tt>:select</tt> - By default, this is "*" as in "SELECT * FROM", but can be changed if you,
+ # * <tt>:select</tt> - By default, this is "*" as in "SELECT * FROM", but can be changed if you,
# for example, want to do a join but not include the joined columns. Takes a string with the SELECT SQL fragment (e.g. "id, name").
- # * <tt>:from</tt> - By default, this is the table name of the class, but can be changed
+ # * <tt>:from</tt> - By default, this is the table name of the class, but can be changed
# to an alternate table name (or even the name of a database view).
# * <tt>:readonly</tt> - Mark the returned records read-only so they cannot be saved or updated.
# * <tt>:lock</tt> - An SQL fragment like "FOR UPDATE" or "LOCK IN SHARE MODE".
@@ -54,7 +54,7 @@ module ActiveRecord
# Person.find(1, 2, 6) # returns an array for objects with IDs in (1, 2, 6)
# Person.find([7, 17]) # returns an array for objects with IDs in (7, 17)
# Person.find([1]) # returns an array for the object with ID = 1
- # Person.find(1, :conditions => "administrator = 1", :order => "created_on DESC")
+ # Person.where("administrator = 1").order("created_on DESC").find(1)
#
# Note that returned records may not be in the same order as the ids you
# provide since database rows are unordered. Give an explicit <tt>:order</tt>
@@ -63,23 +63,23 @@ module ActiveRecord
# ==== Examples
#
# # find first
- # Person.find(:first) # returns the first object fetched by SELECT * FROM people
- # Person.find(:first, :conditions => [ "user_name = ?", user_name])
- # Person.find(:first, :conditions => [ "user_name = :u", { :u => user_name }])
- # Person.find(:first, :order => "created_on DESC", :offset => 5)
+ # Person.first # returns the first object fetched by SELECT * FROM people
+ # Person.where(["user_name = ?", user_name]).first
+ # Person.where(["user_name = :u", { :u => user_name }]).first
+ # Person.order("created_on DESC").offset(5).first
#
# # find last
- # Person.find(:last) # returns the last object fetched by SELECT * FROM people
- # Person.find(:last, :conditions => [ "user_name = ?", user_name])
- # Person.find(:last, :order => "created_on DESC", :offset => 5)
+ # Person.last # returns the last object fetched by SELECT * FROM people
+ # Person.where(["user_name = ?", user_name]).last
+ # Person.order("created_on DESC").offset(5).last
#
# # find all
- # Person.find(:all) # returns an array of objects for all the rows fetched by SELECT * FROM people
- # Person.find(:all, :conditions => [ "category IN (?)", categories], :limit => 50)
- # Person.find(:all, :conditions => { :friends => ["Bob", "Steve", "Fred"] }
- # Person.find(:all, :offset => 10, :limit => 10)
- # Person.find(:all, :include => [ :account, :friends ])
- # Person.find(:all, :group => "category")
+ # Person.all # returns an array of objects for all the rows fetched by SELECT * FROM people
+ # Person.where(["category IN (?)", categories]).limit(50).all
+ # Person.where({ :friends => ["Bob", "Steve", "Fred"] }).all
+ # Person.offset(10).limit(10).all
+ # Person.includes([:account, :friends]).all
+ # Person.group("category").all
#
# Example for find with a lock: Imagine two concurrent transactions:
# each will read <tt>person.visits == 2</tt>, add 1 to it, and save, resulting
@@ -88,7 +88,7 @@ module ActiveRecord
# expected <tt>person.visits == 4</tt>.
#
# Person.transaction do
- # person = Person.find(1, :lock => true)
+ # person = Person.lock(true).find(1)
# person.visits += 1
# person.save!
# end
@@ -123,6 +123,11 @@ module ActiveRecord
end
end
+ # Same as #first! but raises RecordNotFound if no record is returned
+ def first!(*args)
+ self.first(*args) or raise RecordNotFound
+ end
+
# A convenience wrapper for <tt>find(:last, *args)</tt>. You can pass in all the
# same arguments to this method as you can to <tt>find(:last)</tt>.
def last(*args)
@@ -137,6 +142,11 @@ module ActiveRecord
end
end
+ # Same as #last! but raises RecordNotFound if no record is returned
+ def last!(*args)
+ self.last(*args) or raise RecordNotFound
+ end
+
# A convenience wrapper for <tt>find(:all, *args)</tt>. You can pass in all the
# same arguments to this method as you can to <tt>find(:all)</tt>.
def all(*args)
@@ -171,22 +181,25 @@ module ActiveRecord
def exists?(id = nil)
id = id.id if ActiveRecord::Base === id
+ relation = select("1").limit(1)
+
case id
when Array, Hash
- where(id).exists?
+ relation = relation.where(id)
else
- relation = select(primary_key).limit(1)
- relation = relation.where(primary_key.eq(id)) if id
- relation.first ? true : false
+ relation = relation.where(table[primary_key].eq(id)) if id
end
+
+ relation.first ? true : false
end
protected
def find_with_associations
including = (@eager_load_values + @includes_values).uniq
- join_dependency = ActiveRecord::Associations::ClassMethods::JoinDependency.new(@klass, including, nil)
- rows = construct_relation_for_association_find(join_dependency).to_a
+ join_dependency = ActiveRecord::Associations::JoinDependency.new(@klass, including, [])
+ relation = construct_relation_for_association_find(join_dependency)
+ rows = connection.select_all(relation.to_sql, 'SQL', relation.bind_values)
join_dependency.instantiate(rows)
rescue ThrowResult
[]
@@ -194,13 +207,13 @@ module ActiveRecord
def construct_relation_for_association_calculations
including = (@eager_load_values + @includes_values).uniq
- join_dependency = ActiveRecord::Associations::ClassMethods::JoinDependency.new(@klass, including, arel.joins(arel))
+ join_dependency = ActiveRecord::Associations::JoinDependency.new(@klass, including, arel.froms.first)
relation = except(:includes, :eager_load, :preload)
apply_join_dependency(relation, join_dependency)
end
def construct_relation_for_association_find(join_dependency)
- relation = except(:includes, :eager_load, :preload, :select).select(column_aliases(join_dependency))
+ relation = except(:includes, :eager_load, :preload, :select).select(join_dependency.columns)
apply_join_dependency(relation, join_dependency)
end
@@ -222,15 +235,15 @@ module ActiveRecord
end
def construct_limited_ids_condition(relation)
- orders = relation.order_values.join(", ")
- values = @klass.connection.distinct("#{@klass.connection.quote_table_name @klass.table_name}.#{@klass.primary_key}", orders)
+ orders = relation.order_values
+ values = @klass.connection.distinct("#{@klass.connection.quote_table_name table_name}.#{primary_key}", orders)
- ids_array = relation.select(values).collect {|row| row[@klass.primary_key]}
- ids_array.empty? ? raise(ThrowResult) : primary_key.in(ids_array)
+ ids_array = relation.select(values).collect {|row| row[primary_key]}
+ ids_array.empty? ? raise(ThrowResult) : table[primary_key].in(ids_array)
end
def find_by_attributes(match, attributes, *args)
- conditions = attributes.inject({}) {|h, a| h[a] = args[attributes.index(a)]; h}
+ conditions = Hash[attributes.map {|a| [a, args[attributes.index(a)]]}]
result = where(conditions).send(match.finder)
if match.bang? && result.blank?
@@ -288,19 +301,24 @@ module ActiveRecord
def find_one(id)
id = id.id if ActiveRecord::Base === id
- record = where(primary_key.eq(id)).first
+ column = columns_hash[primary_key]
+
+ substitute = connection.substitute_for(column, @bind_values)
+ relation = where(table[primary_key].eq(substitute))
+ relation.bind_values = [[column, id]]
+ record = relation.first
unless record
- conditions = arel.send(:where_clauses).join(', ')
- conditions = " [WHERE #{conditions}]" if conditions.present?
- raise RecordNotFound, "Couldn't find #{@klass.name} with ID=#{id}#{conditions}"
+ conditions = arel.where_sql
+ conditions = " [#{conditions}]" if conditions
+ raise RecordNotFound, "Couldn't find #{@klass.name} with #{primary_key}=#{id}#{conditions}"
end
record
end
def find_some(ids)
- result = where(primary_key.in(ids)).all
+ result = where(table[primary_key].in(ids)).all
expected_size =
if @limit_value && ids.size > @limit_value
@@ -317,7 +335,7 @@ module ActiveRecord
if result.size == expected_size
result
else
- conditions = arel.send(:where_clauses).join(', ')
+ conditions = arel.wheres.map { |x| x.value }.join(', ')
conditions = " [WHERE #{conditions}]" if conditions.present?
error = "Couldn't find all #{@klass.name.pluralize} with IDs "
@@ -342,14 +360,8 @@ module ActiveRecord
end
end
- def column_aliases(join_dependency)
- join_dependency.joins.collect{|join| join.column_names_with_alias.collect{|column_name, aliased_name|
- "#{connection.quote_table_name join.aliased_table_name}.#{connection.quote_column_name column_name} AS #{aliased_name}"}}.flatten.join(", ")
- end
-
def using_limitable_reflections?(reflections)
- reflections.none?(&:collection?)
+ reflections.none? { |r| r.collection? }
end
-
end
end
diff --git a/activerecord/lib/active_record/relation/predicate_builder.rb b/activerecord/lib/active_record/relation/predicate_builder.rb
index 5cea2328e8..982b3d7e9f 100644
--- a/activerecord/lib/active_record/relation/predicate_builder.rb
+++ b/activerecord/lib/active_record/relation/predicate_builder.rb
@@ -1,33 +1,38 @@
module ActiveRecord
- class PredicateBuilder
-
- def initialize(engine)
- @engine = engine
- end
-
- def build_from_hash(attributes, default_table)
+ class PredicateBuilder # :nodoc:
+ def self.build_from_hash(engine, attributes, default_table)
predicates = attributes.map do |column, value|
table = default_table
if value.is_a?(Hash)
- table = Arel::Table.new(column, :engine => @engine)
- build_from_hash(value, table)
+ table = Arel::Table.new(column, engine)
+ build_from_hash(engine, value, table)
else
column = column.to_s
if column.include?('.')
table_name, column = column.split('.', 2)
- table = Arel::Table.new(table_name, :engine => @engine)
+ table = Arel::Table.new(table_name, engine)
end
- attribute = table[column] || Arel::Attribute.new(table, column)
+ attribute = table[column.to_sym]
case value
- when Array, ActiveRecord::Associations::AssociationCollection, ActiveRecord::Relation
- values = value.to_a
+ when ActiveRecord::Relation
+ value.select_values = [value.klass.arel_table['id']] if value.select_values.empty?
+ attribute.in(value.arel.ast)
+ when Array, ActiveRecord::Associations::CollectionProxy
+ values = value.to_a.map { |x|
+ x.is_a?(ActiveRecord::Base) ? x.id : x
+ }
attribute.in(values)
when Range, Arel::Relation
attribute.in(value)
+ when ActiveRecord::Base
+ attribute.eq(value.id)
+ when Class
+ # FIXME: I think we need to deprecate this behavior
+ attribute.eq(value.name)
else
attribute.eq(value)
end
@@ -36,6 +41,5 @@ module ActiveRecord
predicates.flatten
end
-
end
end
diff --git a/activerecord/lib/active_record/relation/query_methods.rb b/activerecord/lib/active_record/relation/query_methods.rb
index e71f1cca72..9470e7c6c5 100644
--- a/activerecord/lib/active_record/relation/query_methods.rb
+++ b/activerecord/lib/active_record/relation/query_methods.rb
@@ -6,247 +6,299 @@ module ActiveRecord
extend ActiveSupport::Concern
attr_accessor :includes_values, :eager_load_values, :preload_values,
- :select_values, :group_values, :order_values, :joins_values, :where_values, :having_values,
+ :select_values, :group_values, :order_values, :joins_values,
+ :where_values, :having_values, :bind_values,
:limit_value, :offset_value, :lock_value, :readonly_value, :create_with_value, :from_value
def includes(*args)
- args.reject! { |a| a.blank? }
- clone.tap {|r| r.includes_values += args if args.present? }
+ args.reject! {|a| a.blank? }
+
+ return self if args.empty?
+
+ relation = clone
+ relation.includes_values = (relation.includes_values + args).flatten.uniq
+ relation
end
def eager_load(*args)
- clone.tap {|r| r.eager_load_values += args if args.present? }
+ return self if args.blank?
+
+ relation = clone
+ relation.eager_load_values += args
+ relation
end
def preload(*args)
- clone.tap {|r| r.preload_values += args if args.present? }
+ return self if args.blank?
+
+ relation = clone
+ relation.preload_values += args
+ relation
end
- def select(*args)
+ def select(value = Proc.new)
if block_given?
- to_a.select {|*block_args| yield(*block_args) }
+ to_a.select {|*block_args| value.call(*block_args) }
else
- clone.tap {|r| r.select_values += args if args.present? }
+ relation = clone
+ relation.select_values += Array.wrap(value)
+ relation
end
end
def group(*args)
- clone.tap {|r| r.group_values += args.flatten if args.present? }
+ return self if args.blank?
+
+ relation = clone
+ relation.group_values += args.flatten
+ relation
end
def order(*args)
- clone.tap {|r| r.order_values += args if args.present? }
- end
+ return self if args.blank?
- def reorder(*args)
- clone.tap {|r| r.order_values = args if args.present? }
+ relation = clone
+ relation.order_values += args.flatten
+ relation
end
def joins(*args)
+ return self if args.compact.blank?
+
+ relation = clone
+
args.flatten!
- clone.tap {|r| r.joins_values += args if args.present? }
+ relation.joins_values += args
+
+ relation
+ end
+
+ def bind(value)
+ relation = clone
+ relation.bind_values += [value]
+ relation
end
def where(opts, *rest)
- value = build_where(opts, rest)
- value ? clone.tap {|r| r.where_values += Array.wrap(value) } : clone
+ return self if opts.blank?
+
+ relation = clone
+ relation.where_values += build_where(opts, rest)
+ relation
end
def having(*args)
- value = build_where(*args)
- clone.tap {|r| r.having_values += Array.wrap(value) if value.present? }
+ return self if args.blank?
+
+ relation = clone
+ relation.having_values += build_where(*args)
+ relation
end
- def limit(value = true)
- clone.tap {|r| r.limit_value = value }
+ def limit(value)
+ relation = clone
+ relation.limit_value = value
+ relation
end
- def offset(value = true)
- clone.tap {|r| r.offset_value = value }
+ def offset(value)
+ relation = clone
+ relation.offset_value = value
+ relation
end
def lock(locks = true)
+ relation = clone
+
case locks
when String, TrueClass, NilClass
- clone.tap {|r| r.lock_value = locks || true }
+ relation.lock_value = locks || true
else
- clone.tap {|r| r.lock_value = false }
+ relation.lock_value = false
end
+
+ relation
end
def readonly(value = true)
- clone.tap {|r| r.readonly_value = value }
+ relation = clone
+ relation.readonly_value = value
+ relation
end
- def create_with(value = true)
- clone.tap {|r| r.create_with_value = value }
+ def create_with(value)
+ relation = clone
+ relation.create_with_value = value && (@create_with_value || {}).merge(value)
+ relation
end
- def from(value = true)
- clone.tap {|r| r.from_value = value }
+ def from(value)
+ relation = clone
+ relation.from_value = value
+ relation
end
- def extending(*modules, &block)
- modules << Module.new(&block) if block_given?
- clone.tap {|r| r.send(:apply_modules, *modules) }
+ def extending(*modules)
+ modules << Module.new(&Proc.new) if block_given?
+
+ return self if modules.empty?
+
+ relation = clone
+ relation.send(:apply_modules, modules.flatten)
+ relation
end
def reverse_order
- order_clause = arel.send(:order_clauses).join(', ')
- relation = except(:order)
+ order_clause = arel.order_clauses
- unless order_clauses.blank?
- relation.order(reverse_sql_order(order_clause))
- else
- relation.order("#{@klass.table_name}.#{@klass.primary_key} DESC")
- end
+ order = order_clause.empty? ?
+ "#{table_name}.#{primary_key} DESC" :
+ reverse_sql_order(order_clause).join(', ')
+
+ except(:order).order(Arel.sql(order))
end
def arel
@arel ||= build_arel
end
- def custom_join_sql(*joins)
- arel = table
- joins.each do |join|
- next if join.blank?
+ def build_arel
+ arel = table.from table
- @implicit_readonly = true
+ build_joins(arel, @joins_values) unless @joins_values.empty?
- case join
- when Hash, Array, Symbol
- if array_of_strings?(join)
- join_string = join.join(' ')
- arel = arel.join(join_string)
- end
- else
- arel = arel.join(join)
- end
- end
- arel.joins(arel)
- end
+ collapse_wheres(arel, (@where_values - ['']).uniq)
- def build_arel
- arel = table
+ arel.having(*@having_values.uniq.reject{|h| h.blank?}) unless @having_values.empty?
- arel = build_joins(arel, @joins_values) unless @joins_values.empty?
+ arel.take(connection.sanitize_limit(@limit_value)) if @limit_value
+ arel.skip(@offset_value) if @offset_value
- @where_values.uniq.each do |where|
- next if where.blank?
+ arel.group(*@group_values.uniq.reject{|g| g.blank?}) unless @group_values.empty?
- case where
- when Arel::SqlLiteral
- arel = arel.where(where)
- else
- sql = where.is_a?(String) ? where : where.to_sql
- arel = arel.where(Arel::SqlLiteral.new("(#{sql})"))
- end
- end
+ arel.order(*@order_values.uniq.reject{|o| o.blank?}) unless @order_values.empty?
- arel = arel.having(*@having_values.uniq.select{|h| h.present?}) unless @having_values.empty?
+ build_select(arel, @select_values.uniq)
- arel = arel.take(@limit_value) if @limit_value
- arel = arel.skip(@offset_value) if @offset_value
+ arel.from(@from_value) if @from_value
+ arel.lock(@lock_value) if @lock_value
- arel = arel.group(*@group_values.uniq.select{|g| g.present?}) unless @group_values.empty?
+ arel
+ end
- arel = arel.order(*@order_values.uniq.select{|o| o.present?}) unless @order_values.empty?
+ private
- arel = build_select(arel, @select_values.uniq)
+ def custom_join_ast(table, joins)
+ joins = joins.reject { |join| join.blank? }
- arel = arel.from(@from_value) if @from_value
- arel = arel.lock(@lock_value) if @lock_value
+ return [] if joins.empty?
- arel
+ @implicit_readonly = true
+
+ joins.map do |join|
+ case join
+ when Array
+ join = Arel.sql(join.join(' ')) if array_of_strings?(join)
+ when String
+ join = Arel.sql(join)
+ end
+ table.create_string_join(join)
+ end
+ end
+
+ def collapse_wheres(arel, wheres)
+ equalities = wheres.grep(Arel::Nodes::Equality)
+
+ arel.where(Arel::Nodes::And.new(equalities)) unless equalities.empty?
+
+ (wheres - equalities).each do |where|
+ where = Arel.sql(where) if String === where
+ arel.where(Arel::Nodes::Grouping.new(where))
+ end
end
def build_where(opts, other = [])
case opts
when String, Array
- @klass.send(:sanitize_sql, other.empty? ? opts : ([opts] + other))
+ [@klass.send(:sanitize_sql, other.empty? ? opts : ([opts] + other))]
when Hash
attributes = @klass.send(:expand_hash_conditions_for_aggregates, opts)
- PredicateBuilder.new(table.engine).build_from_hash(attributes, table)
+ PredicateBuilder.build_from_hash(table.engine, attributes, table)
else
- opts
+ [opts]
end
end
- private
-
- def build_joins(relation, joins)
- joined_associations = []
- association_joins = []
-
- joins = @joins_values.map {|j| j.respond_to?(:strip) ? j.strip : j}.uniq
-
- joins.each do |join|
- association_joins << join if [Hash, Array, Symbol].include?(join.class) && !array_of_strings?(join)
+ def build_joins(manager, joins)
+ buckets = joins.group_by do |join|
+ case join
+ when String
+ 'string_join'
+ when Hash, Symbol, Array
+ 'association_join'
+ when ActiveRecord::Associations::JoinDependency::JoinAssociation
+ 'stashed_join'
+ when Arel::Nodes::Join
+ 'join_node'
+ else
+ raise 'unknown class: %s' % join.class.name
+ end
end
- stashed_association_joins = joins.select {|j| j.is_a?(ActiveRecord::Associations::ClassMethods::JoinDependency::JoinAssociation)}
+ association_joins = buckets['association_join'] || []
+ stashed_association_joins = buckets['stashed_join'] || []
+ join_nodes = buckets['join_node'] || []
+ string_joins = (buckets['string_join'] || []).map { |x|
+ x.strip
+ }.uniq
- non_association_joins = (joins - association_joins - stashed_association_joins)
- custom_joins = custom_join_sql(*non_association_joins)
+ join_list = custom_join_ast(manager, string_joins)
- join_dependency = ActiveRecord::Associations::ClassMethods::JoinDependency.new(@klass, association_joins, custom_joins)
+ join_dependency = ActiveRecord::Associations::JoinDependency.new(
+ @klass,
+ association_joins,
+ join_list
+ )
+
+ join_nodes.each do |join|
+ join_dependency.alias_tracker.aliased_name_for(join.left.name.downcase)
+ end
join_dependency.graft(*stashed_association_joins)
@implicit_readonly = true unless association_joins.empty? && stashed_association_joins.empty?
- to_join = []
-
+ # FIXME: refactor this to build an AST
join_dependency.join_associations.each do |association|
- if (association_relation = association.relation).is_a?(Array)
- to_join << [association_relation.first, association.join_class, association.association_join.first]
- to_join << [association_relation.last, association.join_class, association.association_join.last]
- else
- to_join << [association_relation, association.join_class, association.association_join]
- end
+ association.join_to(manager)
end
- to_join.each do |tj|
- unless joined_associations.detect {|ja| ja[0] == tj[0] && ja[1] == tj[1] && ja[2] == tj[2] }
- joined_associations << tj
- relation = relation.join(tj[0], tj[1]).on(*tj[2])
- end
- end
+ manager.join_sources.concat join_nodes.uniq
+ manager.join_sources.concat join_list
- relation.join(custom_joins)
+ manager
end
def build_select(arel, selects)
- if selects.present?
+ unless selects.empty?
@implicit_readonly = false
- # TODO: fix this ugly hack, we should refactor the callers to get an ARel compatible array.
- # Before this change we were passing to ARel the last element only, and ARel is capable of handling an array
- if selects.all? {|s| s.is_a?(String) || !s.is_a?(Arel::Expression) } && !(selects.last =~ /^COUNT\(/)
- arel.project(*selects)
- else
- arel.project(selects.last)
- end
+ arel.project(*selects)
else
- arel.project(@klass.quoted_table_name + '.*')
+ arel.project(@klass.arel_table[Arel.star])
end
end
def apply_modules(modules)
- values = Array.wrap(modules)
- @extensions += values if values.present?
- values.each {|extension| extend(extension) }
+ unless modules.empty?
+ @extensions += modules
+ modules.each {|extension| extend(extension) }
+ end
end
def reverse_sql_order(order_query)
- order_query.split(',').each { |s|
- if s.match(/\s(asc|ASC)$/)
- s.gsub!(/\s(asc|ASC)$/, ' DESC')
- elsif s.match(/\s(desc|DESC)$/)
- s.gsub!(/\s(desc|DESC)$/, ' ASC')
- else
- s.concat(' DESC')
- end
- }.join(',')
+ order_query.join(', ').split(',').collect do |s|
+ s.gsub!(/\sasc\Z/i, ' DESC') || s.gsub!(/\sdesc\Z/i, ' ASC') || s.concat(' DESC')
+ end
end
def array_of_strings?(o)
diff --git a/activerecord/lib/active_record/relation/spawn_methods.rb b/activerecord/lib/active_record/relation/spawn_methods.rb
index 7712ad2569..128e0fbd86 100644
--- a/activerecord/lib/active_record/relation/spawn_methods.rb
+++ b/activerecord/lib/active_record/relation/spawn_methods.rb
@@ -3,71 +3,109 @@ require 'active_support/core_ext/object/blank'
module ActiveRecord
module SpawnMethods
def merge(r)
+ return self unless r
+ return to_a & r if r.is_a?(Array)
+
merged_relation = clone
- return merged_relation unless r
- ((Relation::ASSOCIATION_METHODS + Relation::MULTI_VALUE_METHODS) - [:joins, :where]).each do |method|
+ Relation::ASSOCIATION_METHODS.each do |method|
value = r.send(:"#{method}_values")
- merged_relation.send(:"#{method}_values=", value) if value.present?
- end
-
- merged_relation = merged_relation.joins(r.joins_values)
- merged_wheres = @where_values
-
- r.where_values.each do |w|
- if w.is_a?(Arel::Predicates::Equality)
- merged_wheres = merged_wheres.reject {|p| p.is_a?(Arel::Predicates::Equality) && p.operand1.name == w.operand1.name }
+ unless value.empty?
+ if method == :includes
+ merged_relation = merged_relation.includes(value)
+ else
+ merged_relation.send(:"#{method}_values=", value)
+ end
end
+ end
- merged_wheres += [w]
+ (Relation::MULTI_VALUE_METHODS - [:joins, :where]).each do |method|
+ value = r.send(:"#{method}_values")
+ merged_relation.send(:"#{method}_values=", merged_relation.send(:"#{method}_values") + value) if value.present?
+ end
+
+ merged_relation.joins_values += r.joins_values
+
+ merged_wheres = @where_values + r.where_values
+
+ unless @where_values.empty?
+ # Remove duplicates, last one wins.
+ seen = Hash.new { |h,table| h[table] = {} }
+ merged_wheres = merged_wheres.reverse.reject { |w|
+ nuke = false
+ if w.respond_to?(:operator) && w.operator == :==
+ name = w.left.name
+ table = w.left.relation.name
+ nuke = seen[table][name]
+ seen[table][name] = true
+ end
+ nuke
+ }.reverse
end
merged_relation.where_values = merged_wheres
- Relation::SINGLE_VALUE_METHODS.reject {|m| m == :lock}.each do |method|
- unless (value = r.send(:"#{method}_value")).nil?
- merged_relation.send(:"#{method}_value=", value)
- end
+ (Relation::SINGLE_VALUE_METHODS - [:lock, :create_with]).each do |method|
+ value = r.send(:"#{method}_value")
+ merged_relation.send(:"#{method}_value=", value) unless value.nil?
end
merged_relation.lock_value = r.lock_value unless merged_relation.lock_value
+ merged_relation = merged_relation.create_with(r.create_with_value) if r.create_with_value
+
# Apply scope extension modules
merged_relation.send :apply_modules, r.extensions
merged_relation
end
- alias :& :merge
-
+ # Removes from the query the condition(s) specified in +skips+.
+ #
+ # Example:
+ #
+ # Post.order('id asc').except(:order) # discards the order condition
+ # Post.where('id > 10').order('id asc').except(:where) # discards the where condition but keeps the order
+ #
def except(*skips)
result = self.class.new(@klass, table)
- (Relation::ASSOCIATION_METHODS + Relation::MULTI_VALUE_METHODS).each do |method|
- result.send(:"#{method}_values=", send(:"#{method}_values")) unless skips.include?(method)
+ ((Relation::ASSOCIATION_METHODS + Relation::MULTI_VALUE_METHODS) - skips).each do |method|
+ result.send(:"#{method}_values=", send(:"#{method}_values"))
end
- Relation::SINGLE_VALUE_METHODS.each do |method|
- result.send(:"#{method}_value=", send(:"#{method}_value")) unless skips.include?(method)
+ (Relation::SINGLE_VALUE_METHODS - skips).each do |method|
+ result.send(:"#{method}_value=", send(:"#{method}_value"))
end
+ # Apply scope extension modules
+ result.send(:apply_modules, extensions)
+
result
end
+ # Removes any condition from the query other than the one(s) specified in +onlies+.
+ #
+ # Example:
+ #
+ # Post.order('id asc').only(:where) # discards the order condition
+ # Post.order('id asc').only(:where, :order) # uses the specified order
+ #
def only(*onlies)
result = self.class.new(@klass, table)
- onlies.each do |only|
- if (Relation::ASSOCIATION_METHODS + Relation::MULTI_VALUE_METHODS).include?(only)
- result.send(:"#{only}_values=", send(:"#{only}_values"))
- elsif Relation::SINGLE_VALUE_METHODS.include?(only)
- result.send(:"#{only}_value=", send(:"#{only}_value"))
- else
- raise "Invalid argument : #{only}"
- end
+ ((Relation::ASSOCIATION_METHODS + Relation::MULTI_VALUE_METHODS) & onlies).each do |method|
+ result.send(:"#{method}_values=", send(:"#{method}_values"))
+ end
+
+ (Relation::SINGLE_VALUE_METHODS & onlies).each do |method|
+ result.send(:"#{method}_value=", send(:"#{method}_value"))
end
+ # Apply scope extension modules
+ result.send(:apply_modules, extensions)
+
result
end
@@ -79,19 +117,16 @@ module ActiveRecord
return relation unless options
options.assert_valid_keys(VALID_FIND_OPTIONS)
+ finders = options.dup
+ finders.delete_if { |key, value| value.nil? && key != :limit }
- [:joins, :select, :group, :having, :limit, :offset, :from, :lock, :readonly].each do |finder|
- relation = relation.send(finder, options[finder]) if options.has_key?(finder)
- end
-
- # Give precedence to newly-applied orders and groups to play nicely with with_scope
- [:group, :order].each do |finder|
- relation.send("#{finder}_values=", Array.wrap(options[finder]) + relation.send("#{finder}_values")) if options.has_key?(finder)
+ ([:joins, :select, :group, :order, :having, :limit, :offset, :from, :lock, :readonly] & finders.keys).each do |finder|
+ relation = relation.send(finder, finders[finder])
end
- relation = relation.where(options[:conditions]) if options.has_key?(:conditions)
- relation = relation.includes(options[:include]) if options.has_key?(:include)
- relation = relation.extending(options[:extend]) if options.has_key?(:extend)
+ relation = relation.where(finders[:conditions]) if options.has_key?(:conditions)
+ relation = relation.includes(finders[:include]) if options.has_key?(:include)
+ relation = relation.extending(finders[:extend]) if options.has_key?(:extend)
relation
end
diff --git a/activerecord/lib/active_record/result.rb b/activerecord/lib/active_record/result.rb
new file mode 100644
index 0000000000..0465b21e88
--- /dev/null
+++ b/activerecord/lib/active_record/result.rb
@@ -0,0 +1,34 @@
+module ActiveRecord
+ ###
+ # This class encapsulates a Result returned from calling +exec+ on any
+ # database connection adapter. For example:
+ #
+ # x = ActiveRecord::Base.connection.exec('SELECT * FROM foo')
+ # x # => #<ActiveRecord::Result:0xdeadbeef>
+ class Result
+ include Enumerable
+
+ attr_reader :columns, :rows
+
+ def initialize(columns, rows)
+ @columns = columns
+ @rows = rows
+ @hash_rows = nil
+ end
+
+ def each
+ hash_rows.each { |row| yield row }
+ end
+
+ def to_hash
+ hash_rows
+ end
+
+ private
+ def hash_rows
+ @hash_rows ||= @rows.map { |row|
+ Hash[@columns.zip(row)]
+ }
+ end
+ end
+end
diff --git a/activerecord/lib/active_record/schema.rb b/activerecord/lib/active_record/schema.rb
index c1bc3214ea..d815ab05ac 100644
--- a/activerecord/lib/active_record/schema.rb
+++ b/activerecord/lib/active_record/schema.rb
@@ -30,10 +30,8 @@ module ActiveRecord
# ActiveRecord::Schema is only supported by database adapters that also
# support migrations, the two features being very similar.
class Schema < Migration
- private_class_method :new
-
- def self.migrations_path
- ActiveRecord::Migrator.migrations_path
+ def migrations_paths
+ ActiveRecord::Migrator.migrations_paths
end
# Eval the given block. All methods available to the current connection
@@ -48,11 +46,12 @@ module ActiveRecord
# ...
# end
def self.define(info={}, &block)
- instance_eval(&block)
+ schema = new
+ schema.instance_eval(&block)
unless info[:version].blank?
initialize_schema_migrations_table
- assume_migrated_upto_version(info[:version], migrations_path)
+ assume_migrated_upto_version(info[:version], schema.migrations_paths)
end
end
end
diff --git a/activerecord/lib/active_record/schema_dumper.rb b/activerecord/lib/active_record/schema_dumper.rb
index e9af20e1b6..a893c0ad85 100644
--- a/activerecord/lib/active_record/schema_dumper.rb
+++ b/activerecord/lib/active_record/schema_dumper.rb
@@ -41,11 +41,11 @@ module ActiveRecord
define_params = @version ? ":version => #{@version}" : ""
stream.puts <<HEADER
-# This file is auto-generated from the current state of the database. Instead
+# This file is auto-generated from the current state of the database. Instead
# of editing this file, please use the migrations feature of Active Record to
# incrementally modify your database, and then regenerate this schema definition.
#
-# Note that this schema.rb definition is the authoritative source for your
+# Note that this schema.rb definition is the authoritative source for your
# database schema. If you need to create the application database on another
# system, you should be using db:schema:load, not running all the migrations
# from scratch. The latter is a flawed and unsustainable approach (the more migrations
@@ -83,7 +83,7 @@ HEADER
# first dump primary key column
if @connection.respond_to?(:pk_and_sequence_for)
- pk, pk_seq = @connection.pk_and_sequence_for(table)
+ pk, _ = @connection.pk_and_sequence_for(table)
elsif @connection.respond_to?(:primary_key)
pk = @connection.primary_key(table)
end
@@ -114,16 +114,16 @@ HEADER
column.type.to_s
end
spec[:limit] = column.limit.inspect if column.limit != @types[column.type][:limit] && spec[:type] != 'decimal'
- spec[:precision] = column.precision.inspect if !column.precision.nil?
- spec[:scale] = column.scale.inspect if !column.scale.nil?
- spec[:null] = 'false' if !column.null
+ spec[:precision] = column.precision.inspect if column.precision
+ spec[:scale] = column.scale.inspect if column.scale
+ spec[:null] = 'false' unless column.null
spec[:default] = default_string(column.default) if column.has_default?
(spec.keys - [:name, :type]).each{ |k| spec[k].insert(0, "#{k.inspect} => ")}
spec
end.compact
# find all migration keys used in this table
- keys = [:name, :limit, :precision, :scale, :default, :null] & column_specs.map(&:keys).flatten
+ keys = [:name, :limit, :precision, :scale, :default, :null] & column_specs.map{ |k| k.keys }.flatten
# figure out the lengths for each column based on above keys
lengths = keys.map{ |key| column_specs.map{ |spec| spec[key] ? spec[key].length + 2 : 0 }.max }
@@ -176,13 +176,15 @@ HEADER
def indexes(table, stream)
if (indexes = @connection.indexes(table)).any?
add_index_statements = indexes.map do |index|
- statement_parts = [ ('add_index ' + index.table.inspect) ]
- statement_parts << index.columns.inspect
- statement_parts << (':name => ' + index.name.inspect)
+ statement_parts = [
+ ('add_index ' + index.table.inspect),
+ index.columns.inspect,
+ (':name => ' + index.name.inspect),
+ ]
statement_parts << ':unique => true' if index.unique
- index_lengths = index.lengths.compact if index.lengths.is_a?(Array)
- statement_parts << (':length => ' + Hash[*index.columns.zip(index.lengths).flatten].inspect) if index_lengths.present?
+ index_lengths = (index.lengths || []).compact
+ statement_parts << (':length => ' + Hash[index.columns.zip(index.lengths)].inspect) unless index_lengths.empty?
' ' + statement_parts.join(', ')
end
diff --git a/activerecord/lib/active_record/serialization.rb b/activerecord/lib/active_record/serialization.rb
index 2d8bd184f3..2bde06f562 100644
--- a/activerecord/lib/active_record/serialization.rb
+++ b/activerecord/lib/active_record/serialization.rb
@@ -5,7 +5,7 @@ module ActiveRecord #:nodoc:
include ActiveModel::Serializers::JSON
def serializable_hash(options = nil)
- options ||= {}
+ options = options.try(:clone) || {}
options[:except] = Array.wrap(options[:except]).map { |n| n.to_s }
options[:except] |= Array.wrap(self.class.inheritance_column)
@@ -22,8 +22,8 @@ module ActiveRecord #:nodoc:
end
private
- # Add associations specified via the <tt>:includes</tt> option.
- #
+ # Add associations specified via the <tt>:include</tt> option.
+ #
# Expects a block that takes as arguments:
# +association+ - name of the association
# +records+ - the association record(s) to be serialized
@@ -45,7 +45,7 @@ module ActiveRecord #:nodoc:
send(association)
end
- unless records.nil?
+ if records
association_options = include_has_options ? include_associations[association] : base_only_or_except
opts = options.merge(association_options)
yield(association, records, opts)
diff --git a/activerecord/lib/active_record/serializers/xml_serializer.rb b/activerecord/lib/active_record/serializers/xml_serializer.rb
index b2d4a48945..8c4adf7116 100644
--- a/activerecord/lib/active_record/serializers/xml_serializer.rb
+++ b/activerecord/lib/active_record/serializers/xml_serializer.rb
@@ -80,7 +80,7 @@ module ActiveRecord #:nodoc:
# closure created by a Proc, to_xml can be used to add elements that normally fall
# outside of the scope of the model -- for example, generating and appending URLs
# associated with models.
- #
+ #
# proc = Proc.new { |options, record| options[:builder].tag!('name-reverse', record.name.reverse) }
# firm.to_xml :procs => [ proc ]
#
@@ -226,17 +226,17 @@ module ActiveRecord #:nodoc:
class Attribute < ActiveModel::Serializers::Xml::Serializer::Attribute #:nodoc:
def compute_type
- type = @serializable.class.serialized_attributes.has_key?(name) ?
- super : @serializable.class.columns_hash[name].type
+ klass = @serializable.class
+ type = if klass.serialized_attributes.key?(name)
+ super
+ elsif klass.columns_hash.key?(name)
+ klass.columns_hash[name].type
+ else
+ NilClass
+ end
- case type
- when :text
- :string
- when :time
- :datetime
- else
- type
- end
+ { :text => :string,
+ :time => :datetime }[type] || type
end
protected :compute_type
end
diff --git a/activerecord/lib/active_record/session_store.rb b/activerecord/lib/active_record/session_store.rb
index becde0fbfd..7e77aefb21 100644
--- a/activerecord/lib/active_record/session_store.rb
+++ b/activerecord/lib/active_record/session_store.rb
@@ -9,7 +9,7 @@ module ActiveRecord
# +id+ (numeric primary key),
# +session_id+ (text, or longtext if your session data exceeds 65K), and
# +data+ (text or longtext; careful if your session data exceeds 65KB).
- #
+ #
# The +session_id+ column should always be indexed for speedy lookups.
# Session data is marshaled to the +data+ column in Base64 format.
# If the data you write is larger than the column's size limit,
@@ -59,17 +59,17 @@ module ActiveRecord
end
def drop_table!
- connection.execute "DROP TABLE #{table_name}"
+ connection_pool.clear_table_cache!(table_name)
+ connection.drop_table table_name
end
def create_table!
- connection.execute <<-end_sql
- CREATE TABLE #{table_name} (
- id #{connection.type_to_sql(:primary_key)},
- #{connection.quote_column_name(session_id_column)} VARCHAR(255) UNIQUE,
- #{connection.quote_column_name(data_column_name)} TEXT
- )
- end_sql
+ connection_pool.clear_table_cache!(table_name)
+ connection.create_table(table_name) do |t|
+ t.string session_id_column, :limit => 255
+ t.text data_column_name
+ end
+ connection.add_index table_name, session_id_column, :unique => true
end
end
@@ -205,6 +205,7 @@ module ActiveRecord
class << self
alias :data_column_name :data_column
+ remove_method :connection
def connection
@@connection ||= ActiveRecord::Base.connection
end
@@ -289,6 +290,7 @@ module ActiveRecord
self.session_class = Session
SESSION_RECORD_KEY = 'rack.session.record'
+ ENV_SESSION_OPTIONS_KEY = Rack::Session::Abstract::ENV_SESSION_OPTIONS_KEY
private
def get_session(env, sid)
@@ -300,7 +302,7 @@ module ActiveRecord
end
end
- def set_session(env, sid, session_data)
+ def set_session(env, sid, session_data, options)
Base.silence do
record = get_session_model(env, sid)
record.data = session_data
@@ -317,12 +319,15 @@ module ActiveRecord
sid
end
- def destroy(env)
+ def destroy_session(env, session_id, options)
if sid = current_session_id(env)
Base.silence do
get_session_model(env, sid).destroy
+ env[SESSION_RECORD_KEY] = nil
end
end
+
+ generate_sid unless options[:drop]
end
def get_session_model(env, sid)
diff --git a/activerecord/lib/active_record/test_case.rb b/activerecord/lib/active_record/test_case.rb
index e61a378d17..29efbbcb8c 100644
--- a/activerecord/lib/active_record/test_case.rb
+++ b/activerecord/lib/active_record/test_case.rb
@@ -1,8 +1,18 @@
module ActiveRecord
# = Active Record Test Case
- #
+ #
# Defines some test assertions to test against SQL queries.
class TestCase < ActiveSupport::TestCase #:nodoc:
+ setup :cleanup_identity_map
+
+ def setup
+ cleanup_identity_map
+ end
+
+ def cleanup_identity_map
+ ActiveRecord::IdentityMap.clear
+ end
+
def assert_date_from_db(expected, actual, message = nil)
# SybaseAdapter doesn't have a separate column type just for dates,
# so the time is in the string and incorrectly formatted
@@ -16,12 +26,13 @@ module ActiveRecord
def assert_sql(*patterns_to_match)
$queries_executed = []
yield
+ $queries_executed
ensure
failed_patterns = []
patterns_to_match.each do |pattern|
failed_patterns << pattern unless $queries_executed.any?{ |sql| pattern === sql }
end
- assert failed_patterns.empty?, "Query pattern(s) #{failed_patterns.map(&:inspect).join(', ')} not found.#{$queries_executed.size == 0 ? '' : "\nQueries:\n#{$queries_executed.join("\n")}"}"
+ assert failed_patterns.empty?, "Query pattern(s) #{failed_patterns.map{ |p| p.inspect }.join(', ')} not found.#{$queries_executed.size == 0 ? '' : "\nQueries:\n#{$queries_executed.join("\n")}"}"
end
def assert_queries(num = 1)
@@ -36,21 +47,6 @@ module ActiveRecord
assert_queries(0, &block)
end
- def self.use_concurrent_connections
- setup :connection_allow_concurrency_setup
- teardown :connection_allow_concurrency_teardown
- end
-
- def connection_allow_concurrency_setup
- @connection = ActiveRecord::Base.remove_connection
- ActiveRecord::Base.establish_connection(@connection.merge({:allow_concurrency => true}))
- end
-
- def connection_allow_concurrency_teardown
- 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
diff --git a/activerecord/lib/active_record/timestamp.rb b/activerecord/lib/active_record/timestamp.rb
index 5531d12a41..1511c71ffc 100644
--- a/activerecord/lib/active_record/timestamp.rb
+++ b/activerecord/lib/active_record/timestamp.rb
@@ -1,72 +1,83 @@
+require 'active_support/core_ext/class/attribute'
+
module ActiveRecord
# = Active Record Timestamp
- #
+ #
# Active Record automatically timestamps create and update operations if the
- # table has fields named <tt>created_at/created_on</tt> or
+ # table has fields named <tt>created_at/created_on</tt> or
# <tt>updated_at/updated_on</tt>.
#
# Timestamping can be turned off by setting:
#
- # <tt>ActiveRecord::Base.record_timestamps = false</tt>
+ # config.active_record.record_timestamps = false
#
# Timestamps are in the local timezone by default but you can use UTC by setting:
#
- # <tt>ActiveRecord::Base.default_timezone = :utc</tt>
+ # config.active_record.default_timezone = :utc
#
# == Time Zone aware attributes
#
# By default, ActiveRecord::Base keeps all the datetime columns time zone aware by executing following code.
#
- # ActiveRecord::Base.time_zone_aware_attributes = true
+ # config.active_record.time_zone_aware_attributes = true
#
# This feature can easily be turned off by assigning value <tt>false</tt> .
#
- # If your attributes are time zone aware and you desire to skip time zone conversion for certain
- # attributes then you can do following:
+ # If your attributes are time zone aware and you desire to skip time zone conversion to the current Time.zone
+ # when reading certain attributes then you can do following:
#
- # Topic.skip_time_zone_conversion_for_attributes = [:written_on]
+ # class Topic < ActiveRecord::Base
+ # self.skip_time_zone_conversion_for_attributes = [:written_on]
+ # end
module Timestamp
extend ActiveSupport::Concern
included do
- class_inheritable_accessor :record_timestamps, :instance_writer => false
+ class_attribute :record_timestamps, :instance_writer => false
self.record_timestamps = true
end
private
def create #:nodoc:
- if record_timestamps
+ if self.record_timestamps
current_time = current_time_from_proper_timezone
- timestamp_attributes_for_create.each do |column|
+ all_timestamp_attributes.each do |column|
write_attribute(column.to_s, current_time) if respond_to?(column) && self.send(column).nil?
end
-
- timestamp_attributes_for_update_in_model.each do |column|
- write_attribute(column.to_s, current_time) if self.send(column).nil?
- end
end
super
end
def update(*args) #:nodoc:
- record_update_timestamps if !partial_updates? || changed?
+ if should_record_timestamps?
+ current_time = current_time_from_proper_timezone
+
+ timestamp_attributes_for_update_in_model.each do |column|
+ column = column.to_s
+ next if attribute_changed?(column)
+ write_attribute(column, current_time)
+ end
+ end
super
end
- def record_update_timestamps #:nodoc:
- return unless record_timestamps
- current_time = current_time_from_proper_timezone
- timestamp_attributes_for_update_in_model.inject({}) do |hash, column|
- hash[column.to_s] = write_attribute(column.to_s, current_time)
- hash
- end
+ def should_record_timestamps?
+ self.record_timestamps && (!partial_updates? || changed? || (attributes.keys & self.class.serialized_attributes.keys).present?)
+ end
+
+ def timestamp_attributes_for_create_in_model
+ timestamp_attributes_for_create.select { |c| self.class.column_names.include?(c.to_s) }
+ end
+
+ def timestamp_attributes_for_update_in_model
+ timestamp_attributes_for_update.select { |c| self.class.column_names.include?(c.to_s) }
end
- def timestamp_attributes_for_update_in_model #:nodoc:
- timestamp_attributes_for_update.select { |elem| respond_to?(elem) }
+ def all_timestamp_attributes_in_model
+ timestamp_attributes_for_create_in_model + timestamp_attributes_for_update_in_model
end
def timestamp_attributes_for_update #:nodoc:
@@ -78,9 +89,9 @@ module ActiveRecord
end
def all_timestamp_attributes #:nodoc:
- timestamp_attributes_for_update + timestamp_attributes_for_create
+ timestamp_attributes_for_create + timestamp_attributes_for_update
end
-
+
def current_time_from_proper_timezone #:nodoc:
self.class.default_timezone == :utc ? Time.now.utc : Time.now
end
diff --git a/activerecord/lib/active_record/transactions.rb b/activerecord/lib/active_record/transactions.rb
index a7709b9489..60d4c256c4 100644
--- a/activerecord/lib/active_record/transactions.rb
+++ b/activerecord/lib/active_record/transactions.rb
@@ -11,6 +11,7 @@ module ActiveRecord
included do
define_callbacks :commit, :rollback, :terminator => "result == false", :scope => [:kind, :name]
end
+
# = Active Record Transactions
#
# Transactions are protective blocks where SQL statements are only permanent
@@ -130,7 +131,7 @@ module ActiveRecord
#
# +transaction+ calls can be nested. By default, this makes all database
# statements in the nested transaction block become part of the parent
- # transaction. For example:
+ # transaction. For example, the following behavior may be surprising:
#
# User.transaction do
# User.create(:username => 'Kotori')
@@ -140,12 +141,15 @@ module ActiveRecord
# end
# end
#
- # User.find(:all) # => empty
+ # creates both "Kotori" and "Nemu". Reason is the <tt>ActiveRecord::Rollback</tt>
+ # exception in the nested block does not issue a ROLLBACK. Since these exceptions
+ # are captured in transaction blocks, the parent block does not see it and the
+ # real transaction is committed.
#
- # It is also possible to requires a sub-transaction by passing
- # <tt>:requires_new => true</tt>. If anything goes wrong, the
- # database rolls back to the beginning of the sub-transaction
- # without rolling back the parent transaction. For example:
+ # In order to get a ROLLBACK for the nested transaction you may ask for a real
+ # sub-transaction by passing <tt>:requires_new => true</tt>. If anything goes wrong,
+ # the database rolls back to the beginning of the sub-transaction without rolling
+ # back the parent transaction. If we add it to the previous example:
#
# User.transaction do
# User.create(:username => 'Kotori')
@@ -155,12 +159,12 @@ module ActiveRecord
# end
# end
#
- # User.find(:all) # => Returns only Kotori
+ # only "Kotori" is created. (This works on MySQL and PostgreSQL, but not on SQLite3.)
#
# Most databases don't support true nested transactions. At the time of
# writing, the only database that we're aware of that supports true nested
# transactions, is MS-SQL. Because of this, Active Record emulates nested
- # transactions by using savepoints. See
+ # transactions by using savepoints on MySQL and PostgreSQL. See
# http://dev.mysql.com/doc/refman/5.0/en/savepoints.html
# for more information about savepoints.
#
@@ -224,8 +228,8 @@ module ActiveRecord
end
# See ActiveRecord::Transactions::ClassMethods for detailed documentation.
- def transaction(&block)
- self.class.transaction(&block)
+ def transaction(options = {}, &block)
+ self.class.transaction(options, &block)
end
def destroy #:nodoc:
@@ -247,6 +251,7 @@ module ActiveRecord
remember_transaction_record_state
yield
rescue Exception
+ IdentityMap.remove(self) if IdentityMap.enabled?
restore_transaction_record_state
raise
ensure
@@ -255,7 +260,7 @@ module ActiveRecord
# Call the after_commit callbacks
def committed! #:nodoc:
- _run_commit_callbacks
+ run_callbacks :commit
ensure
clear_transaction_record_state
end
@@ -263,7 +268,7 @@ module ActiveRecord
# Call the after rollback callbacks. The restore_state argument indicates if the record
# state should be rolled back to the beginning or just to the last savepoint.
def rolledback!(force_restore_state = false) #:nodoc:
- _run_rollback_callbacks
+ run_callbacks :rollback
ensure
restore_transaction_record_state(force_restore_state)
end
@@ -297,8 +302,8 @@ module ActiveRecord
# Save the new record state and id of a record so it can be restored later if a transaction fails.
def remember_transaction_record_state #:nodoc
@_start_transaction_state ||= {}
+ @_start_transaction_state[:id] = id if has_attribute?(self.class.primary_key)
unless @_start_transaction_state.include?(:new_record)
- @_start_transaction_state[:id] = id if has_attribute?(self.class.primary_key)
@_start_transaction_state[:new_record] = @new_record
end
unless @_start_transaction_state.include?(:destroyed)
@@ -321,16 +326,14 @@ module ActiveRecord
@_start_transaction_state[:level] = (@_start_transaction_state[:level] || 0) - 1
if @_start_transaction_state[:level] < 1
restore_state = remove_instance_variable(:@_start_transaction_state)
- if restore_state
- @attributes = @attributes.dup if @attributes.frozen?
- @new_record = restore_state[:new_record]
- @destroyed = restore_state[:destroyed]
- if restore_state[:id]
- self.id = restore_state[:id]
- else
- @attributes.delete(self.class.primary_key)
- @attributes_cache.delete(self.class.primary_key)
- end
+ @attributes = @attributes.dup if @attributes.frozen?
+ @new_record = restore_state[:new_record]
+ @destroyed = restore_state[:destroyed]
+ if restore_state.has_key?(:id)
+ self.id = restore_state[:id]
+ else
+ @attributes.delete(self.class.primary_key)
+ @attributes_cache.delete(self.class.primary_key)
end
end
end
diff --git a/activerecord/lib/active_record/validations.rb b/activerecord/lib/active_record/validations.rb
index b98fd353aa..d73fce9fd0 100644
--- a/activerecord/lib/active_record/validations.rb
+++ b/activerecord/lib/active_record/validations.rb
@@ -1,5 +1,5 @@
module ActiveRecord
- # = Active Record Validations
+ # = Active Record RecordInvalid
#
# Raised by <tt>save!</tt> and <tt>create!</tt> when the record is invalid. Use the
# +record+ method to retrieve the record which did not validate.
@@ -18,6 +18,13 @@ module ActiveRecord
end
end
+ # = Active Record Validations
+ #
+ # Active Record includes the majority of its validations from <tt>ActiveModel::Validations</tt>
+ # all of which accept the <tt>:on</tt> argument to define the context where the
+ # validations are active. Active Record will always supply either the context of
+ # <tt>:create</tt> or <tt>:update</tt> dependent on whether the model is a
+ # <tt>new_record?</tt>.
module Validations
extend ActiveSupport::Concern
include ActiveModel::Validations
@@ -37,7 +44,7 @@ module ActiveRecord
end
end
- # The validation process on save can be skipped by passing false. The regular Base#save method is
+ # The validation process on save can be skipped by passing :validate => false. The regular Base#save method is
# replaced with this when the validations module is mixed in, which it is by default.
def save(options={})
perform_validations(options) ? super : false
@@ -49,33 +56,25 @@ module ActiveRecord
perform_validations(options) ? super : raise(RecordInvalid.new(self))
end
- # Runs all the specified validations and returns true if no errors were added otherwise false.
+ # Runs all the validations within the specified context. Returns true if no errors are found,
+ # false otherwise.
+ #
+ # If the argument is false (default is +nil+), the context is set to <tt>:create</tt> if
+ # <tt>new_record?</tt> is true, and to <tt>:update</tt> if it is not.
+ #
+ # Validations with no <tt>:on</tt> option will run no matter the context. Validations with
+ # some <tt>:on</tt> option will only run in the specified context.
def valid?(context = nil)
context ||= (new_record? ? :create : :update)
output = super(context)
-
- deprecated_callback_method(:validate)
- deprecated_callback_method(:"validate_on_#{context}")
-
errors.empty? && output
end
protected
def perform_validations(options={})
- perform_validation = case options
- when Hash
- options[:validate] != false
- else
- ActiveSupport::Deprecation.warn "save(#{options}) is deprecated, please give save(:validate => #{options}) instead", caller
- options
- end
-
- if perform_validation
- valid?(options.is_a?(Hash) ? options[:context] : nil)
- else
- true
- end
+ perform_validation = options[:validate] != false
+ perform_validation ? valid?(options[:context]) : true
end
end
end
diff --git a/activerecord/lib/active_record/validations/associated.rb b/activerecord/lib/active_record/validations/associated.rb
index 15b587de45..3a783aeb00 100644
--- a/activerecord/lib/active_record/validations/associated.rb
+++ b/activerecord/lib/active_record/validations/associated.rb
@@ -27,13 +27,15 @@ module ActiveRecord
#
# this would specify a circular dependency and cause infinite recursion.
#
- # NOTE: This validation will not fail if the association hasn't been assigned. If you want to
- # ensure that the association is both present and guaranteed to be valid, you also need to
+ # NOTE: This validation will not fail if the association hasn't been assigned. If you want to
+ # ensure that the association is both present and guaranteed to be valid, you also need to
# use +validates_presence_of+.
#
# Configuration options:
# * <tt>:message</tt> - A custom error message (default is: "is invalid")
- # * <tt>:on</tt> - Specifies when this validation is active (default is <tt>:save</tt>, other options <tt>:create</tt>, <tt>:update</tt>).
+ # * <tt>:on</tt> - Specifies when this validation is active. Runs in all
+ # validation contexts by default (+nil+), other options are <tt>:create</tt>
+ # and <tt>:update</tt>.
# * <tt>:if</tt> - Specifies a method, proc or string to call to determine if the validation should
# occur (e.g. <tt>:if => :allow_validation</tt>, or <tt>:if => Proc.new { |user| user.signup_step > 2 }</tt>). The
# method, proc or string should return or evaluate to a true or false value.
diff --git a/activerecord/lib/active_record/validations/uniqueness.rb b/activerecord/lib/active_record/validations/uniqueness.rb
index bf863c7063..9cd6c26322 100644
--- a/activerecord/lib/active_record/validations/uniqueness.rb
+++ b/activerecord/lib/active_record/validations/uniqueness.rb
@@ -14,19 +14,23 @@ module ActiveRecord
def validate_each(record, attribute, value)
finder_class = find_finder_class_for(record)
- table = finder_class.unscoped
- table_name = record.class.quoted_table_name
- sql, params = mount_sql_and_params(finder_class, table_name, attribute, value)
+ coder = record.class.serialized_attributes[attribute.to_s]
- relation = table.where(sql, *params)
+ if value && coder
+ value = coder.dump value
+ end
+
+ sql, params = mount_sql_and_params(finder_class, record.class.quoted_table_name, attribute, value)
+
+ relation = finder_class.unscoped.where(sql, *params)
Array.wrap(options[:scope]).each do |scope_item|
scope_value = record.send(scope_item)
relation = relation.where(scope_item => scope_value)
end
- unless record.new_record?
+ if record.persisted?
# TODO : This should be in Arel
relation = relation.where("#{record.class.quoted_table_name}.#{record.class.primary_key} <> ?", record.send(:id))
end
@@ -78,24 +82,29 @@ module ActiveRecord
end
module ClassMethods
- # Validates whether the value of the specified attributes are unique across the system.
+ # Validates whether the value of the specified attributes are unique across the system.
# Useful for making sure that only one user
# can be named "davidhh".
#
# class Person < ActiveRecord::Base
- # validates_uniqueness_of :user_name, :scope => :account_id
+ # validates_uniqueness_of :user_name
# end
#
- # It can also validate whether the value of the specified attributes are unique based on multiple
- # scope parameters. For example, making sure that a teacher can only be on the schedule once
+ # It can also validate whether the value of the specified attributes are unique based on a scope parameter:
+ #
+ # class Person < ActiveRecord::Base
+ # validates_uniqueness_of :user_name, :scope => :account_id
+ # end
+ #
+ # Or even multiple scope parameters. For example, making sure that a teacher can only be on the schedule once
# per semester for a particular class.
#
# class TeacherSchedule < ActiveRecord::Base
# validates_uniqueness_of :teacher_id, :scope => [:semester_id, :class_id]
# end
#
- # When the record is created, a check is performed to make sure that no record exists in the database
- # with the given value for the specified attribute (that maps to a column). When the record is updated,
+ # When the record is created, a check is performed to make sure that no record exists in the database
+ # with the given value for the specified attribute (that maps to a column). When the record is updated,
# the same check is made but disregarding the record itself.
#
# Configuration options:
@@ -105,11 +114,11 @@ module ActiveRecord
# * <tt>:allow_nil</tt> - If set to true, skips this validation if the attribute is +nil+ (default is +false+).
# * <tt>:allow_blank</tt> - If set to true, skips this validation if the attribute is blank (default is +false+).
# * <tt>:if</tt> - Specifies a method, proc or string to call to determine if the validation should
- # occur (e.g. <tt>:if => :allow_validation</tt>, or <tt>:if => Proc.new { |user| user.signup_step > 2 }</tt>).
+ # occur (e.g. <tt>:if => :allow_validation</tt>, or <tt>:if => Proc.new { |user| user.signup_step > 2 }</tt>).
# The method, proc or string should return or evaluate to a true or false value.
# * <tt>:unless</tt> - Specifies a method, proc or string to call to determine if the validation should
- # not occur (e.g. <tt>:unless => :skip_validation</tt>, or
- # <tt>:unless => Proc.new { |user| user.signup_step <= 2 }</tt>). The method, proc or string should
+ # not occur (e.g. <tt>:unless => :skip_validation</tt>, or
+ # <tt>:unless => Proc.new { |user| user.signup_step <= 2 }</tt>). The method, proc or string should
# return or evaluate to a true or false value.
#
# === Concurrency and integrity
@@ -149,33 +158,32 @@ module ActiveRecord
# | # title!
#
# This could even happen if you use transactions with the 'serializable'
- # isolation level. There are several ways to get around this problem:
+ # isolation level. The best way to work around this problem is to add a unique
+ # index to the database table using
+ # ActiveRecord::ConnectionAdapters::SchemaStatements#add_index. In the
+ # rare case that a race condition occurs, the database will guarantee
+ # the field's uniqueness.
+ #
+ # When the database catches such a duplicate insertion,
+ # ActiveRecord::Base#save will raise an ActiveRecord::StatementInvalid
+ # exception. You can either choose to let this error propagate (which
+ # will result in the default Rails exception page being shown), or you
+ # can catch it and restart the transaction (e.g. by telling the user
+ # that the title already exists, and asking him to re-enter the title).
+ # This technique is also known as optimistic concurrency control:
+ # http://en.wikipedia.org/wiki/Optimistic_concurrency_control
#
- # - By locking the database table before validating, and unlocking it after
- # saving. However, table locking is very expensive, and thus not
- # recommended.
- # - By locking a lock file before validating, and unlocking it after saving.
- # This does not work if you've scaled your Rails application across
- # multiple web servers (because they cannot share lock files, or cannot
- # do that efficiently), and thus not recommended.
- # - Creating a unique index on the field, by using
- # ActiveRecord::ConnectionAdapters::SchemaStatements#add_index. In the
- # rare case that a race condition occurs, the database will guarantee
- # the field's uniqueness.
- #
- # When the database catches such a duplicate insertion,
- # ActiveRecord::Base#save will raise an ActiveRecord::StatementInvalid
- # exception. You can either choose to let this error propagate (which
- # will result in the default Rails exception page being shown), or you
- # can catch it and restart the transaction (e.g. by telling the user
- # that the title already exists, and asking him to re-enter the title).
- # This technique is also known as optimistic concurrency control:
- # http://en.wikipedia.org/wiki/Optimistic_concurrency_control
- #
- # Active Record currently provides no way to distinguish unique
- # index constraint errors from other types of database errors, so you
- # will have to parse the (database-specific) exception message to detect
- # such a case.
+ # The bundled ActiveRecord::ConnectionAdapters distinguish unique index
+ # constraint errors from other types of database errors by throwing an
+ # ActiveRecord::RecordNotUnique exception.
+ # For other adapters you will have to parse the (database-specific) exception
+ # message to detect such a case.
+ # The following bundled adapters throw the ActiveRecord::RecordNotUnique exception:
+ # * ActiveRecord::ConnectionAdapters::MysqlAdapter
+ # * ActiveRecord::ConnectionAdapters::Mysql2Adapter
+ # * ActiveRecord::ConnectionAdapters::SQLiteAdapter
+ # * ActiveRecord::ConnectionAdapters::SQLite3Adapter
+ # * ActiveRecord::ConnectionAdapters::PostgreSQLAdapter
#
def validates_uniqueness_of(*attr_names)
validates_with UniquenessValidator, _merge_attributes(attr_names)
diff --git a/activerecord/lib/active_record/version.rb b/activerecord/lib/active_record/version.rb
index a467ffa960..0667be7d23 100644
--- a/activerecord/lib/active_record/version.rb
+++ b/activerecord/lib/active_record/version.rb
@@ -1,10 +1,10 @@
module ActiveRecord
module VERSION #:nodoc:
MAJOR = 3
- MINOR = 0
+ MINOR = 1
TINY = 0
- BUILD = "rc"
+ PRE = "beta"
- STRING = [MAJOR, MINOR, TINY, BUILD].join('.')
+ STRING = [MAJOR, MINOR, TINY, PRE].compact.join('.')
end
end