diff options
Diffstat (limited to 'activerecord/lib/active_record')
133 files changed, 8308 insertions, 6039 deletions
diff --git a/activerecord/lib/active_record/aggregations.rb b/activerecord/lib/active_record/aggregations.rb index 5a8addc4e4..3db8e0716b 100644 --- a/activerecord/lib/active_record/aggregations.rb +++ b/activerecord/lib/active_record/aggregations.rb @@ -10,9 +10,9 @@ module ActiveRecord # 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 + # 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 @@ -46,7 +46,7 @@ module ActiveRecord # # def <=>(other_money) # if currency == other_money.currency - # amount <=> amount + # amount <=> other_money.amount # else # amount <=> other_money.exchange_to(currency).amount # end @@ -71,7 +71,7 @@ module ActiveRecord # 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: + # objects just like you would with any other attribute: # # customer.balance = Money.new(20) # sets the Money value object and the attribute # customer.balance # => Money value object @@ -86,6 +86,12 @@ module ActiveRecord # customer.address_street = "Hyancintvej" # customer.address_city = "Copenhagen" # customer.address # => Address.new("Hyancintvej", "Copenhagen") + # + # customer.address_street = "Vesterbrogade" + # customer.address # => Address.new("Hyancintvej", "Copenhagen") + # customer.clear_aggregation_cache + # customer.address # => Address.new("Vesterbrogade", "Copenhagen") + # # customer.address = Address.new("May Street", "Chicago") # customer.address_street # => "May Street" # customer.address_city # => "Chicago" @@ -101,8 +107,8 @@ module ActiveRecord # 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 amount changed after creation. Create a new Money object with the new value instead. The + # Money#exchange_to method is an example of this. It 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. # @@ -119,7 +125,7 @@ module ActiveRecord # 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 + # 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. # @@ -187,7 +193,8 @@ module ActiveRecord # * <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>. + # not an instance of <tt>:class_name</tt>. If <tt>:allow_nil</tt> is set to true, the converter + # can return nil to skip the assignment. # # Option examples: # composed_of :temperature, :mapping => %w(reading celsius) @@ -216,7 +223,7 @@ module ActiveRecord reader_method(name, class_name, mapping, allow_nil, constructor) writer_method(name, class_name, mapping, allow_nil, converter) - create_reflection(:composed_of, part_id, options, self) + create_reflection(:composed_of, part_id, nil, options, self) end private @@ -235,16 +242,15 @@ module ActiveRecord def writer_method(name, class_name, mapping, allow_nil, converter) define_method("#{name}=") do |part| + klass = class_name.constantize + unless part.is_a?(klass) || converter.nil? || part.nil? + part = converter.respond_to?(:call) ? converter.call(part) : klass.send(converter, part) + end + 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 diff --git a/activerecord/lib/active_record/associations.rb b/activerecord/lib/active_record/associations.rb index 958821add6..60b7118d7e 100644 --- a/activerecord/lib/active_record/associations.rb +++ b/activerecord/lib/active_record/associations.rb @@ -1,9 +1,6 @@ require 'active_support/core_ext/enumerable' -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: @@ -20,7 +17,7 @@ module ActiveRecord 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}'.") + super("Cannot have a has_many :through association '#{owner_class_name}##{reflection.name}' on the polymorphic object '#{source_reflection.class_name}##{source_reflection.name}' without 'source_type'. Try adding 'source_type: \"#{reflection.name.to_s.classify}\"' to 'has_many :through' definition.") end end @@ -104,6 +101,7 @@ module ActiveRecord # See ActiveRecord::Associations::ClassMethods for documentation. module Associations # :nodoc: + extend ActiveSupport::Autoload extend ActiveSupport::Concern # These classes will be loaded when associations are created. @@ -133,11 +131,13 @@ module ActiveRecord 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' + eager_autoload do + 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' + end # Clears out the association cache. def clear_association_cache #:nodoc: @@ -195,26 +195,6 @@ module ActiveRecord # * <tt>Project#categories.empty?, Project#categories.size, Project#categories, Project#categories<<(category1),</tt> # <tt>Project#categories.delete(category1)</tt> # - # === Overriding generated methods - # - # Association methods are generated in a module that is included into the model class, - # which allows you to easily override with your own methods and call the original - # generated method with +super+. For example: - # - # class Car < ActiveRecord::Base - # belongs_to :owner - # belongs_to :old_owner - # def owner=(new_owner) - # self.old_owner = self.owner - # super - # end - # end - # - # If your model class is <tt>Project</tt>, the module is - # named <tt>Project::GeneratedFeatureMethods</tt>. The GeneratedFeatureMethods module is - # included in the model class immediately after the (anonymous) generated attributes methods - # module, meaning an association will override the methods for an attribute with the same name. - # # === A word of warning # # Don't create associations that have the same name as instance methods of @@ -262,6 +242,26 @@ module ActiveRecord # others.uniq | X | X | X # others.reset | X | X | X # + # === Overriding generated methods + # + # Association methods are generated in a module that is included into the model class, + # which allows you to easily override with your own methods and call the original + # generated method with +super+. For example: + # + # class Car < ActiveRecord::Base + # belongs_to :owner + # belongs_to :old_owner + # def owner=(new_owner) + # self.old_owner = self.owner + # super + # end + # end + # + # If your model class is <tt>Project</tt>, the module is + # named <tt>Project::GeneratedFeatureMethods</tt>. The GeneratedFeatureMethods module is + # included in the model class immediately after the (anonymous) generated attributes methods + # module, meaning an association will override the methods for an attribute with the same name. + # # == Cardinality and associations # # Active Record associations can be used to describe one-to-one, one-to-many and many-to-many @@ -397,7 +397,28 @@ module ActiveRecord # * All unsaved (<tt>new_record? == true</tt>) members of the collection are automatically # saved when the parent is saved. # - # === Association callbacks + # == Customizing the query + # + # Associations are built from <tt>Relation</tt>s, and you can use the <tt>Relation</tt> syntax + # to customize them. For example, to add a condition: + # + # class Blog < ActiveRecord::Base + # has_many :published_posts, -> { where published: true }, class_name: 'Post' + # end + # + # Inside the <tt>-> { ... }</tt> block you can use all of the usual <tt>Relation</tt> methods. + # + # === Accessing the owner object + # + # Sometimes it is useful to have access to the owner object when building the query. The owner + # is passed as a parameter to the block. For example, the following association would find all + # events that occur on the user's birthday: + # + # class User < ActiveRecord::Base + # has_many :birthday_events, ->(user) { where starts_on: user.birthday }, class_name: 'Event' + # end + # + # == Association callbacks # # 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 @@ -424,7 +445,7 @@ module ActiveRecord # added to the collection. Same with the +before_remove+ callbacks; if an exception is # thrown the object doesn't get removed. # - # === Association extensions + # == 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 @@ -454,20 +475,11 @@ module ActiveRecord # end # # class Account < ActiveRecord::Base - # has_many :people, :extend => FindOrCreateByNameExtension + # has_many :people, -> { extending FindOrCreateByNameExtension } # end # # class Company < ActiveRecord::Base - # has_many :people, :extend => FindOrCreateByNameExtension - # end - # - # 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. - # - # class Account < ActiveRecord::Base - # has_many :people, :extend => [FindOrCreateByNameExtension, FindRecentExtension] + # has_many :people, -> { extending FindOrCreateByNameExtension } # end # # Some extensions can only be made to work with knowledge of the association's internals. @@ -485,7 +497,7 @@ module ActiveRecord # the same object, allowing you to make calls like <tt>proxy_association.owner</tt> inside # association extensions. # - # === Association Join Models + # == 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 @@ -543,7 +555,7 @@ module ActiveRecord # end # # @group = Group.first - # @group.users.collect { |u| u.avatar }.flatten # select all avatars for all users in the group + # @group.users.collect { |u| u.avatar }.compact # 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 @@ -569,7 +581,7 @@ module ActiveRecord # belongs_to :tag, :inverse_of => :taggings # end # - # === Nested Associations + # == 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: @@ -612,7 +624,7 @@ module ActiveRecord # 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 # # 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 @@ -742,7 +754,7 @@ module ActiveRecord # to include an association which has conditions defined on it: # # class Post < ActiveRecord::Base - # has_many :approved_comments, :class_name => 'Comment', :conditions => ['approved = ?', true] + # has_many :approved_comments, -> { where approved: true }, :class_name => 'Comment' # end # # Post.includes(:approved_comments) @@ -754,14 +766,11 @@ module ActiveRecord # returning all the associated objects: # # class Picture < ActiveRecord::Base - # has_many :most_recent_comments, :class_name => 'Comment', :order => 'id DESC', :limit => 10 + # has_many :most_recent_comments, -> { order('id DESC').limit(10) }, :class_name => 'Comment' # end # # Picture.includes(:most_recent_comments).first.most_recent_comments # => returns all associated comments. # - # 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. # # class Address < ActiveRecord::Base @@ -839,8 +848,8 @@ module ActiveRecord # module MyApplication # module Business # class Firm < ActiveRecord::Base - # has_many :clients - # end + # has_many :clients + # end # # class Client < ActiveRecord::Base; end # end @@ -938,7 +947,8 @@ module ActiveRecord # # 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. + # specific association types. When no option is given, the behaviour is to do nothing + # with the associated records when destroying a record. # # === Delete or destroy? # @@ -1077,15 +1087,6 @@ module ActiveRecord # 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 - # 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+ @@ -1093,44 +1094,18 @@ module ActiveRecord # [:primary_key] # Specify the method that returns the primary key used for the association. By default this is +id+. # [:dependent] - # If set to <tt>:destroy</tt> all the associated objects are destroyed - # alongside this object by calling their +destroy+ method. If set to <tt>:delete_all</tt> all associated - # objects are deleted *without* calling their +destroy+ method. If set to <tt>:nullify</tt> all associated - # objects' foreign keys are set to +NULL+ *without* calling their +save+ callbacks. If set to - # <tt>:restrict</tt> an error will be added to the object, preventing its deletion, if any associated - # objects are present. + # Controls what happens to the associated objects when + # their owner is destroyed: + # + # * <tt>:destroy</tt> causes all the associated objects to also be destroyed + # * <tt>:delete_all</tt> causes all the asssociated objects to be deleted directly from the database (so callbacks will not execute) + # * <tt>:nullify</tt> causes the foreign keys to be set to +NULL+. Callbacks are not executed. + # * <tt>:restrict_with_exception</tt> causes an exception to be raised if there are any associated records + # * <tt>:restrict_with_error</tt> causes an error to be added to the owner if there are any associated objects # # 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. May be supplied as a string or a proc where interpolation is - # required. 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 - # replacing <tt>SELECT ... FROM</tt> with <tt>SELECT COUNT(*) FROM</tt>. - # [:extend] - # Specify a named module for extending the proxy. See "Association extensions". - # [:include] - # Specify second-order associations that should be eager loaded when the collection is loaded. - # [: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. 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, - # 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 - # to include the primary and foreign keys, otherwise it will raise an error. # [:as] # Specifies a polymorphic interface (See <tt>belongs_to</tt>). # [:through] @@ -1157,16 +1132,16 @@ module ActiveRecord # [:source_type] # Specifies type of the source association used by <tt>has_many :through</tt> queries where the source # association is a polymorphic +belongs_to+. - # [:uniq] - # If true, duplicates will be omitted from the collection. Useful in conjunction with <tt>:through</tt>. - # [: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. # [: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. - # By default, only save associated objects that are new records. + # By default, only save associated objects that are new records. This option is implemented as a + # before_save callback. Because callbacks are run in the order they are defined, associated objects + # may need to be explicitly saved in any user-defined before_save callbacks. + # + # Note that <tt>accepts_nested_attributes_for</tt> sets <tt>:autosave</tt> to <tt>true</tt>. # [: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 @@ -1174,24 +1149,16 @@ module ActiveRecord # See ActiveRecord::Associations::ClassMethods's overview on Bi-directional associations for more detail. # # Option examples: - # has_many :comments, :order => "posted_on" - # has_many :comments, :include => :author - # has_many :people, :class_name => "Person", :conditions => "deleted = 0", :order => "name" - # has_many :tracks, :order => "position", :dependent => :destroy - # has_many :comments, :dependent => :nullify - # has_many :tags, :as => :taggable - # has_many :reports, :readonly => true - # has_many :subscribers, :through => :subscriptions, :source => :user - # has_many :subscribers, :class_name => "Person", :finder_sql => Proc.new { - # %Q{ - # SELECT DISTINCT * - # 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(name, options = {}, &extension) - Builder::HasMany.build(self, name, options, &extension) + # has_many :comments, -> { order "posted_on" } + # has_many :comments, -> { includes :author } + # has_many :people, -> { where("deleted = 0").order("name") }, class_name: "Person" + # has_many :tracks, -> { order "position" }, dependent: :destroy + # has_many :comments, dependent: :nullify + # has_many :tags, as: :taggable + # has_many :reports, -> { readonly } + # has_many :subscribers, through: :subscriptions, source: :user + def has_many(name, scope = nil, options = {}, &extension) + Builder::HasMany.build(self, name, scope, options, &extension) end # Specifies a one-to-one association with another class. This method should only be used @@ -1239,34 +1206,23 @@ module ActiveRecord # Specify the class name of the association. Use it only if that name can't be inferred # from the association name. So <tt>has_one :manager</tt> will by default be linked to the Manager class, but # if the real class name is Person, you'll have to specify it with this option. - # [: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 - # 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+. - # If set to <tt>:restrict</tt>, an error will be added to the object, preventing its deletion, if an - # associated object is present. + # Controls what happens to the associated object when + # its owner is destroyed: + # + # * <tt>:destroy</tt> causes the associated object to also be destroyed + # * <tt>:delete</tt> causes the asssociated object to be deleted directly from the database (so callbacks will not execute) + # * <tt>:nullify</tt> causes the foreign key to be set to +NULL+. Callbacks are not executed. + # * <tt>:restrict_with_exception</tt> causes an exception to be raised if there is an associated record + # * <tt>:restrict_with_error</tt> causes an error to be added to the owner if there is an associated object # [: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 # 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+. - # [:include] - # Specify second-order associations that should be eager loaded when this object is loaded. # [: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 - # 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>, # <tt>:primary_key</tt>, and <tt>:foreign_key</tt> are ignored, as the association uses the @@ -1280,14 +1236,14 @@ module ActiveRecord # [:source_type] # Specifies type of the source association used by <tt>has_one :through</tt> queries where the source # association is a polymorphic +belongs_to+. - # [:readonly] - # If true, the associated object is readonly through the association. # [:validate] # If +false+, don't validate the associated object when saving the parent object. +false+ by default. # [:autosave] # If true, always save the associated object or destroy it if marked for destruction, # when saving the parent object. If false, never save or destroy the associated object. # By default, only save the associated object if it's a new record. + # + # Note that <tt>accepts_nested_attributes_for</tt> sets <tt>:autosave</tt> to <tt>true</tt>. # [: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 @@ -1298,14 +1254,14 @@ module ActiveRecord # has_one :credit_card, :dependent => :destroy # destroys the associated credit card # 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'" - # has_one :attachment, :as => :attachable - # 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(name, options = {}) - Builder::HasOne.build(self, name, options) + # has_one :last_comment, -> { order 'posted_on' }, :class_name => "Comment" + # has_one :project_manager, -> { where role: 'project_manager' }, :class_name => "Person" + # has_one :attachment, as: :attachable + # has_one :boss, readonly: :true + # has_one :club, through: :membership + # has_one :primary_address, -> { where primary: true }, through: :addressables, source: :addressable + def has_one(name, scope = nil, options = {}) + Builder::HasOne.build(self, name, scope, options) end # Specifies a one-to-one association with another class. This method should only be used @@ -1350,13 +1306,6 @@ module ActiveRecord # Specify the class name of the association. Use it only if that name can't be inferred # from the association name. So <tt>belongs_to :author</tt> will by default be linked to the Author class, but # if the real class name is Person, you'll have to specify it with this option. - # [: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>. - # [: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 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> @@ -1382,19 +1331,17 @@ module ActiveRecord # 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 + # is used on the associate class (such as a Post class) - that is the migration for + # <tt>#{table_name}_count</tt> is created on the associate class (such that Post.comments_count will + # return the count cached, see note below). 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 # using +attr_readonly+. - # [:include] - # Specify second-order associations that should be eager loaded when this object is loaded. # [:polymorphic] # Specify this association is a polymorphic association by passing +true+. # Note: If you've enabled the counter cache, then you may want to add the counter cache attribute # to the +attr_readonly+ list in the associated classes (e.g. <tt>class Post; attr_readonly :comments_count; end</tt>). - # [: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. # [:autosave] @@ -1402,6 +1349,8 @@ module ActiveRecord # 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. + # + # Note that <tt>accepts_nested_attributes_for</tt> sets <tt>:autosave</tt> to <tt>true</tt>. # [: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 @@ -1413,24 +1362,24 @@ module ActiveRecord # See ActiveRecord::Associations::ClassMethods's overview on Bi-directional associations for more detail. # # Option examples: - # belongs_to :firm, :foreign_key => "client_of" - # belongs_to :person, :primary_key => "name", :foreign_key => "person_name" - # belongs_to :author, :class_name => "Person", :foreign_key => "author_id" - # belongs_to :valid_coupon, :class_name => "Coupon", :foreign_key => "coupon_id", - # :conditions => 'discounts > #{payments_count}' - # belongs_to :attachable, :polymorphic => true - # belongs_to :project, :readonly => true - # belongs_to :post, :counter_cache => true - # belongs_to :company, :touch => true - # belongs_to :company, :touch => :employees_last_updated_at - def belongs_to(name, options = {}) - Builder::BelongsTo.build(self, name, options) + # belongs_to :firm, foreign_key: "client_of" + # belongs_to :person, primary_key: "name", foreign_key: "person_name" + # belongs_to :author, class_name: "Person", foreign_key: "author_id" + # belongs_to :valid_coupon, ->(o) { where "discounts > #{o.payments_count}" }, + # class_name: "Coupon", foreign_key: "coupon_id" + # belongs_to :attachable, polymorphic: true + # belongs_to :project, readonly: true + # belongs_to :post, counter_cache: true + # belongs_to :company, touch: true + # belongs_to :company, touch: :employees_last_updated_at + def belongs_to(name, scope = nil, options = {}) + Builder::BelongsTo.build(self, name, scope, 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". + # will give the default join table name of "developers_projects" because "D" precedes "P" alphabetically. # 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 @@ -1512,8 +1461,8 @@ module ActiveRecord # * <tt>Developer#projects.size</tt> # * <tt>Developer#projects.find(id)</tt> # * <tt>Developer#projects.exists?(...)</tt> - # * <tt>Developer#projects.build</tt> (similar to <tt>Project.new("project_id" => id)</tt>) - # * <tt>Developer#projects.create</tt> (similar to <tt>c = Project.new("project_id" => id); c.save; c</tt>) + # * <tt>Developer#projects.build</tt> (similar to <tt>Project.new("developer_id" => id)</tt>) + # * <tt>Developer#projects.create</tt> (similar to <tt>c = Project.new("developer_id" => id); c.save; c</tt>) # The declaration may include an options hash to specialize the behavior of the association. # # === Options @@ -1536,47 +1485,6 @@ module ActiveRecord # By default this is guessed to be the name of the associated class in lower-case and "_id" suffixed. # So if a Person class makes a +has_and_belongs_to_many+ association to Project, # the association will use "project_id" as the default <tt>:association_foreign_key</tt>. - # [:conditions] - # Specify the conditions that the associated object must meet in order to be included as a +WHERE+ - # SQL fragment, such as <tt>authorized = 1</tt>. Record creations from the association are - # scoped if a hash is used. - # <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> - # [:uniq] - # If true, duplicate associated objects will be ignored by accessors and query methods. - # [:finder_sql] - # 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 - # 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 - # classes with a manual statement. - # [:insert_sql] - # Overwrite the default generated SQL statement used to add links between the associated classes - # with a manual statement. - # [:extend] - # Anonymous module for extending the proxy, see "Association extensions". - # [:include] - # Specify second-order associations that should be eager loaded when the collection is loaded. - # [: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. - # 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, - # 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 - # and foreign keys, otherwise it will raise an error. # [:readonly] # If true, all the associated objects are readonly through the association. # [:validate] @@ -1587,16 +1495,16 @@ module ActiveRecord # If false, never save or destroy the associated objects. # By default, only save associated objects that are new records. # + # Note that <tt>accepts_nested_attributes_for</tt> sets <tt>:autosave</tt> to <tt>true</tt>. + # # Option examples: # has_and_belongs_to_many :projects - # has_and_belongs_to_many :projects, :include => [ :milestones, :manager ] - # has_and_belongs_to_many :nations, :class_name => "Country" - # has_and_belongs_to_many :categories, :join_table => "prods_cats" - # 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(name, options = {}, &extension) - Builder::HasAndBelongsToMany.build(self, name, options, &extension) + # has_and_belongs_to_many :projects, -> { includes :milestones, :manager } + # has_and_belongs_to_many :nations, class_name: "Country" + # has_and_belongs_to_many :categories, join_table: "prods_cats" + # has_and_belongs_to_many :categories, -> { readonly } + def has_and_belongs_to_many(name, scope = nil, options = {}, &extension) + Builder::HasAndBelongsToMany.build(self, name, scope, options, &extension) end end end diff --git a/activerecord/lib/active_record/associations/association.rb b/activerecord/lib/active_record/associations/association.rb index 7887d59aad..495f0cde59 100644 --- a/activerecord/lib/active_record/associations/association.rb +++ b/activerecord/lib/active_record/associations/association.rb @@ -1,5 +1,4 @@ require 'active_support/core_ext/array/wrap' -require 'active_support/core_ext/object/inclusion' module ActiveRecord module Associations @@ -25,9 +24,7 @@ module ActiveRecord def initialize(owner, reflection) reflection.check_validity! - @target = nil @owner, @reflection = owner, reflection - @updated = false reset reset_scope @@ -38,14 +35,14 @@ module ActiveRecord # post.comments.aliased_table_name # => "comments" # def aliased_table_name - reflection.klass.table_name + 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 + @stale_state = nil end # Reloads the \target and returns +self+ on success. @@ -83,10 +80,15 @@ module ActiveRecord loaded! end - def scoped + def scope target_scope.merge(association_scope) end + def scoped + ActiveSupport::Deprecation.warn("#scoped is deprecated. use #scope instead.") + scope + end + # The scope for this association. # # Note that the association_scope is merged into the target_scope only when the @@ -120,7 +122,7 @@ module ActiveRecord # Can be overridden (i.e. in ThroughAssociation) to merge in other scopes (i.e. the # through association's scope) def target_scope - klass.scoped + klass.all end # Loads the \target if needed and returns it. @@ -134,17 +136,8 @@ module ActiveRecord # ActiveRecord::RecordNotFound is rescued within the method, and it is # not reraised. The proxy is \reset and +nil+ is the return value. def load_target - 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 + @target = find_target if (@stale_state && stale_target?) || find_target? + loaded! unless loaded? target rescue ActiveRecord::RecordNotFound @@ -159,6 +152,21 @@ module ActiveRecord end end + # We can't dump @reflection since it contains the scope proc + def marshal_dump + reflection = @reflection + @reflection = nil + + ivars = instance_variables.map { |name| [name, instance_variable_get(name)] } + [reflection.name, ivars] + end + + def marshal_load(data) + reflection_name, ivars = data + ivars.each { |name, val| instance_variable_set(name, val) } + @reflection = @owner.class.reflect_on_association(reflection_name) + end + private def find_target? @@ -168,7 +176,7 @@ module ActiveRecord def creation_attributes attributes = {} - if reflection.macro.in?([:has_one, :has_many]) && !options[:through] + if (reflection.macro == :has_one || reflection.macro == :has_many) && !options[:through] attributes[reflection.foreign_key] = owner[reflection.active_record_primary_key] if reflection.options[:as] @@ -225,13 +233,10 @@ module ActiveRecord def stale_state end - def association_class - @reflection.klass - end - - def build_record(attributes, options) - reflection.build_association(attributes, options) do |record| - record.assign_attributes(create_scope.except(*record.changed), :without_protection => true) + def build_record(attributes) + reflection.build_association(attributes) do |record| + attributes = create_scope.except(*(record.changed - [reflection.foreign_key])) + record.assign_attributes(attributes) end end end diff --git a/activerecord/lib/active_record/associations/association_scope.rb b/activerecord/lib/active_record/associations/association_scope.rb index 982084c9b8..1303822868 100644 --- a/activerecord/lib/active_record/associations/association_scope.rb +++ b/activerecord/lib/active_record/associations/association_scope.rb @@ -6,7 +6,7 @@ module ActiveRecord attr_reader :association, :alias_tracker delegate :klass, :owner, :reflection, :interpolate, :to => :association - delegate :chain, :conditions, :options, :source_options, :active_record, :to => :reflection + delegate :chain, :scope_chain, :options, :source_options, :active_record, :to => :reflection def initialize(association) @association = association @@ -15,23 +15,28 @@ module ActiveRecord def scope scope = klass.unscoped - scope = scope.extending(*Array(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, :references, :order, :limit, :joins, :group, :having, :offset, :select)) + scope.merge! eval_scope(klass, reflection.scope) if reflection.scope + add_constraints(scope) + end - if options[:through] && !options[:include] - scope = scope.includes(source_options[:include]) - end + private - scope = scope.uniq if options[:uniq] + def column_for(table_name, column_name) + columns = alias_tracker.connection.schema_cache.columns_hash[table_name] + columns[column_name] + end - add_constraints(scope) + def bind_value(scope, column, value) + substitute = alias_tracker.connection.substitute_at( + column, scope.bind_values.length) + scope.bind_values += [[column, value]] + substitute end - private + def bind(scope, table_name, column_name, value) + column = column_for table_name, column_name + bind_value scope, column, value + end def add_constraints(scope) tables = construct_tables @@ -64,21 +69,14 @@ module ActiveRecord foreign_key = reflection.active_record_primary_key end - conditions = self.conditions[i] - if reflection == chain.last - scope = scope.where(table[key].eq(owner[foreign_key])) + bind_val = bind scope, table.table_name, key.to_s, owner[foreign_key] + scope = scope.where(table[key].eq(bind_val)) if reflection.type - scope = scope.where(table[reflection.type].eq(owner.class.base_class.name)) - end - - conditions.each do |condition| - if options[:through] && condition.is_a?(Hash) - condition = { table.name => condition } - end - - scope = scope.where(interpolate(condition)) + value = owner.class.base_class.name + bind_val = bind scope, table.table_name, reflection.type.to_s, value + scope = scope.where(table[reflection.type].eq(bind_val)) end else constraint = table[key].eq(foreign_table[foreign_key]) @@ -89,13 +87,15 @@ module ActiveRecord end scope = scope.joins(join(foreign_table, constraint)) + end - conditions.each do |condition| - condition = interpolate(condition) - condition = { (table.table_alias || table.name) => condition } unless i == 0 + # Exclude the scope of the association itself, because that + # was already merged in the #scope method. + (scope_chain[i] - [self.reflection.scope]).each do |scope_chain_item| + item = eval_scope(reflection.klass, scope_chain_item) - scope = scope.where(condition) - end + scope.includes! item.includes_values + scope.where_values += item.where_values end end @@ -117,6 +117,13 @@ module ActiveRecord end end + def eval_scope(klass, scope) + if scope.is_a?(Relation) + scope + else + klass.unscoped.instance_exec(owner, &scope) + 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 97f531d064..75f72c1a46 100644 --- a/activerecord/lib/active_record/associations/belongs_to_association.rb +++ b/activerecord/lib/active_record/associations/belongs_to_association.rb @@ -2,6 +2,11 @@ module ActiveRecord # = Active Record Belongs To Associations module Associations class BelongsToAssociation < SingularAssociation #:nodoc: + + def handle_dependency + target.send(options[:dependent]) if load_target + end + def replace(record) raise_on_type_mismatch(record) if record @@ -14,6 +19,11 @@ module ActiveRecord self.target = record end + def reset + super + @updated = false + end + def updated? @updated end @@ -72,7 +82,7 @@ module ActiveRecord end def stale_state - owner[reflection.foreign_key].to_s + owner[reflection.foreign_key] && 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 2ee5dbbd70..88ce03a3cd 100644 --- a/activerecord/lib/active_record/associations/belongs_to_polymorphic_association.rb +++ b/activerecord/lib/active_record/associations/belongs_to_polymorphic_association.rb @@ -27,7 +27,8 @@ module ActiveRecord end def stale_state - [super, owner[reflection.foreign_type].to_s] + foreign_key = super + foreign_key && [foreign_key.to_s, 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 index 2059d8acdf..1df876bf62 100644 --- a/activerecord/lib/active_record/associations/builder/association.rb +++ b/activerecord/lib/active_record/associations/builder/association.rb @@ -1,86 +1,106 @@ module ActiveRecord::Associations::Builder class Association #:nodoc: - class_attribute :valid_options - self.valid_options = [:class_name, :foreign_key, :select, :conditions, :include, :extend, :readonly, :validate, :references] + class << self + attr_accessor :valid_options + end - # Set by subclasses - class_attribute :macro + self.valid_options = [:class_name, :foreign_key, :validate] - attr_reader :model, :name, :options, :reflection + attr_reader :model, :name, :scope, :options, :reflection - def self.build(model, name, options) - new(model, name, options).build + def self.build(*args, &block) + new(*args, &block).build end - def initialize(model, name, options) - @model, @name, @options = model, name, options + def initialize(model, name, scope, options) + @model = model + @name = name + + if scope.is_a?(Hash) + @scope = nil + @options = scope + else + @scope = scope + @options = options + end + + if @scope && @scope.arity == 0 + prev_scope = @scope + @scope = proc { instance_exec(&prev_scope) } + end end def mixin @model.generated_feature_methods end + include Module.new { def build; end } + def build validate_options - reflection = model.create_reflection(self.class.macro, name, options, model) define_accessors - reflection + configure_dependency if options[:dependent] + @reflection = model.create_reflection(macro, name, scope, options, model) + super # provides an extension point + @reflection end - private + def macro + raise NotImplementedError + end - def validate_options - options.assert_valid_keys(self.class.valid_options) - end + def valid_options + Association.valid_options + end - def define_accessors - define_readers - define_writers - end + def validate_options + options.assert_valid_keys(valid_options) + end - def define_readers - name = self.name - mixin.redefine_method(name) do |*params| - association(name).reader(*params) + def define_accessors + define_readers + define_writers + end + + def define_readers + mixin.class_eval <<-CODE, __FILE__, __LINE__ + 1 + def #{name}(*args) + association(:#{name}).reader(*args) end - end + CODE + end - def define_writers - name = self.name - mixin.redefine_method("#{name}=") do |value| - association(name).writer(value) + def define_writers + mixin.class_eval <<-CODE, __FILE__, __LINE__ + 1 + def #{name}=(value) + association(:#{name}).writer(value) end - end + CODE + end - def dependent_restrict_raises? - ActiveRecord::Base.dependent_restrict_raises == true + def configure_dependency + unless valid_dependent_options.include? options[:dependent] + raise ArgumentError, "The :dependent option must be one of #{valid_dependent_options}, but is :#{options[:dependent]}" end - def dependent_restrict_deprecation_warning - if dependent_restrict_raises? - msg = "In the next release, `:dependent => :restrict` will not raise a `DeleteRestrictionError`. "\ - "Instead, it will add an error on the model. To fix this warning, make sure your code " \ - "isn't relying on a `DeleteRestrictionError` and then add " \ - "`config.active_record.dependent_restrict_raises = false` to your application config." - ActiveSupport::Deprecation.warn msg - end + if options[:dependent] == :restrict + ActiveSupport::Deprecation.warn( + "The :restrict option is deprecated. Please use :restrict_with_exception instead, which " \ + "provides the same functionality." + ) end - def define_restrict_dependency_method - name = self.name - mixin.redefine_method(dependency_method_name) do - # has_many or has_one associations - if send(name).respond_to?(:exists?) ? send(name).exists? : !send(name).nil? - if dependent_restrict_raises? - raise ActiveRecord::DeleteRestrictionError.new(name) - else - key = association(name).reflection.macro == :has_one ? "one" : "many" - errors.add(:base, :"restrict_dependent_destroy.#{key}", - :record => self.class.human_attribute_name(name).downcase) - return false - end - end + mixin.class_eval <<-CODE, __FILE__, __LINE__ + 1 + def #{macro}_dependent_for_#{name} + association(:#{name}).handle_dependency end - end + CODE + + model.before_destroy "#{macro}_dependent_for_#{name}" + end + + def valid_dependent_options + raise NotImplementedError + 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 index 4183c222de..2f2600b7fb 100644 --- a/activerecord/lib/active_record/associations/builder/belongs_to.rb +++ b/activerecord/lib/active_record/associations/builder/belongs_to.rb @@ -1,10 +1,12 @@ -require 'active_support/core_ext/object/inclusion' - module ActiveRecord::Associations::Builder class BelongsTo < SingularAssociation #:nodoc: - self.macro = :belongs_to + def macro + :belongs_to + end - self.valid_options += [:foreign_type, :polymorphic, :touch] + def valid_options + super + [:foreign_type, :polymorphic, :touch] + end def constructable? !options[:polymorphic] @@ -14,74 +16,51 @@ module ActiveRecord::Associations::Builder 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 - 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}" - mixin.redefine_method(method_name) do - record = send(name) - record.class.increment_counter(cache_column, record.id) unless record.nil? + mixin.class_eval <<-CODE, __FILE__, __LINE__ + 1 + def belongs_to_counter_cache_after_create_for_#{name} + record = #{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}" - mixin.redefine_method(method_name) do + def belongs_to_counter_cache_before_destroy_for_#{name} unless marked_for_destruction? - record = send(name) - record.class.decrement_counter(cache_column, record.id) unless record.nil? + record = #{name} + record.class.decrement_counter(:#{cache_column}, record.id) unless record.nil? end end - model.before_destroy(method_name) + CODE - 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 + model.after_create "belongs_to_counter_cache_after_create_for_#{name}" + model.before_destroy "belongs_to_counter_cache_before_destroy_for_#{name}" - def add_touch_callbacks(reflection) - name = self.name - method_name = "belongs_to_touch_after_save_or_destroy_for_#{name}" - touch = options[:touch] + klass = reflection.class_name.safe_constantize + klass.attr_readonly cache_column if klass && klass.respond_to?(:attr_readonly) + end - mixin.redefine_method(method_name) do - record = send(name) + def add_touch_callbacks(reflection) + mixin.class_eval <<-CODE, __FILE__, __LINE__ + 1 + def belongs_to_touch_after_save_or_destroy_for_#{name} + record = #{name} unless record.nil? - if touch == true - record.touch - else - record.touch(touch) - end + record.touch #{options[:touch].inspect if options[:touch] != true} end end + CODE - model.after_save(method_name) - model.after_touch(method_name) - model.after_destroy(method_name) - end - - def configure_dependency - if options[:dependent] - unless options[:dependent].in?([:destroy, :delete]) - raise ArgumentError, "The :dependent option expects either :destroy or :delete (#{options[:dependent].inspect})" - end + model.after_save "belongs_to_touch_after_save_or_destroy_for_#{name}" + model.after_touch "belongs_to_touch_after_save_or_destroy_for_#{name}" + model.after_destroy "belongs_to_touch_after_save_or_destroy_for_#{name}" + 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 + def valid_dependent_options + [:destroy, :delete] + 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 index 768f70b6c9..1b382f7285 100644 --- a/activerecord/lib/active_record/associations/builder/collection_association.rb +++ b/activerecord/lib/active_record/associations/builder/collection_association.rb @@ -2,23 +2,19 @@ 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 + def valid_options + super + [:table_name, :finder_sql, :counter_sql, :before_add, :after_add, :before_remove, :after_remove] end - def initialize(model, name, options, &extension) - super(model, name, options) + attr_reader :block_extension, :extension_module + + def initialize(*args, &extension) + super(*args) @block_extension = extension end def build + show_deprecation_warnings wrap_block_extension reflection = super CALLBACKS.each { |callback_name| define_callback(callback_name) } @@ -29,47 +25,61 @@ module ActiveRecord::Associations::Builder true end - private + def show_deprecation_warnings + [:finder_sql, :counter_sql].each do |name| + if options.include? name + ActiveSupport::Deprecation.warn("The :#{name} association option is deprecated. Please find an alternative (such as using scopes).") + end + end + end + + def wrap_block_extension + if block_extension + @extension_module = mod = Module.new(&block_extension) + silence_warnings do + model.parent.const_set(extension_module_name, mod) + end - def wrap_block_extension - options[:extend] = Array(options[:extend]) + prev_scope = @scope - 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) + if prev_scope + @scope = proc { |owner| instance_exec(owner, &prev_scope).extending(mod) } + else + @scope = proc { extending(mod) } end end + end - def extension_module_name - @extension_module_name ||= "#{model.to_s.demodulize}#{name.to_s.camelize}AssociationExtension" - end + def extension_module_name + @extension_module_name ||= "#{model.name.demodulize}#{name.to_s.camelize}AssociationExtension" + end - def define_callback(callback_name) - full_callback_name = "#{callback_name}_for_#{name}" + 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(options[callback_name.to_sym])) - end + # 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(options[callback_name.to_sym])) + end - def define_readers - super + def define_readers + super - name = self.name - mixin.redefine_method("#{name.to_s.singularize}_ids") do - association(name).ids_reader + mixin.class_eval <<-CODE, __FILE__, __LINE__ + 1 + def #{name.to_s.singularize}_ids + association(:#{name}).ids_reader end - end + CODE + end - def define_writers - super + def define_writers + super - name = self.name - mixin.redefine_method("#{name.to_s.singularize}_ids=") do |ids| - association(name).ids_writer(ids) + mixin.class_eval <<-CODE, __FILE__, __LINE__ + 1 + def #{name.to_s.singularize}_ids=(ids) + association(:#{name}).ids_writer(ids) end - end + CODE + 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 index 0b634ab944..bdac02b5bf 100644 --- 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 @@ -1,57 +1,39 @@ module ActiveRecord::Associations::Builder class HasAndBelongsToMany < CollectionAssociation #:nodoc: - self.macro = :has_and_belongs_to_many + def macro + :has_and_belongs_to_many + end - self.valid_options += [:join_table, :association_foreign_key, :delete_sql, :insert_sql] + def valid_options + super + [:join_table, :association_foreign_key, :delete_sql, :insert_sql] + end def build reflection = super - check_validity(reflection) define_destroy_hook reflection end - private + def show_deprecation_warnings + super - def define_destroy_hook - name = self.name - model.send(:include, Module.new { - class_eval <<-RUBY, __FILE__, __LINE__ + 1 - def destroy_associations - association(#{name.to_sym.inspect}).delete_all_on_destroy - super - end - RUBY - }) - 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) + [:delete_sql, :insert_sql].each do |name| + if options.include? name + ActiveSupport::Deprecation.warn("The :#{name} association option is deprecated. Please find an alternative (such as using has_many :through).") end - - reflection.options[:join_table] ||= join_table_name( - model.send(:undecorated_table_name, model.to_s), - model.send(:undecorated_table_name, reflection.class_name) - ) 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 + def define_destroy_hook + name = self.name + model.send(:include, Module.new { + class_eval <<-RUBY, __FILE__, __LINE__ + 1 + def destroy_associations + association(:#{name}).delete_all + super + end + RUBY + }) + 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 index 9ddfd433e4..ab8225460a 100644 --- a/activerecord/lib/active_record/associations/builder/has_many.rb +++ b/activerecord/lib/active_record/associations/builder/has_many.rb @@ -1,60 +1,15 @@ -require 'active_support/core_ext/object/inclusion' - 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 + def macro + :has_many end - private - - def configure_dependency - if options[:dependent] - unless options[:dependent].in?([:destroy, :delete_all, :nullify, :restrict]) - raise ArgumentError, "The :dependent option expects either :destroy, :delete_all, " \ - ":nullify or :restrict (#{options[:dependent].inspect})" - end - - dependent_restrict_deprecation_warning if options[:dependent] == :restrict - send("define_#{options[:dependent]}_dependency_method") - model.before_destroy dependency_method_name - end - end - - def define_destroy_dependency_method - name = self.name - mixin.redefine_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 - o.mark_for_destruction - end - - send(name).delete_all - end - end - - def define_delete_all_dependency_method - name = self.name - mixin.redefine_method(dependency_method_name) do - association(name).delete_all_on_destroy - end - end - - def define_nullify_dependency_method - name = self.name - mixin.redefine_method(dependency_method_name) do - send(name).delete_all - end - end + def valid_options + super + [:primary_key, :dependent, :as, :through, :source, :source_type, :inverse_of] + end - def dependency_method_name - "has_many_dependent_for_#{name}" - end + def valid_dependent_options + [:destroy, :delete_all, :nullify, :restrict, :restrict_with_error, :restrict_with_exception] + 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 index bc8a212bee..0da564f402 100644 --- a/activerecord/lib/active_record/associations/builder/has_one.rb +++ b/activerecord/lib/active_record/associations/builder/has_one.rb @@ -1,56 +1,25 @@ -require 'active_support/core_ext/object/inclusion' - module ActiveRecord::Associations::Builder class HasOne < SingularAssociation #:nodoc: - self.macro = :has_one - - self.valid_options += [:order, :as] + def macro + :has_one + end - class_attribute :through_options - self.through_options = [:through, :source, :source_type] + def valid_options + valid = super + [:order, :as] + valid += [:through, :source, :source_type] if options[:through] + valid + end def constructable? !options[:through] end - def build - reflection = super - configure_dependency unless options[:through] - reflection + def configure_dependency + super unless options[:through] 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 options[:dependent].in?([:destroy, :delete, :nullify, :restrict]) - raise ArgumentError, "The :dependent option expects either :destroy, :delete, " \ - ":nullify or :restrict (#{options[:dependent].inspect})" - end - - dependent_restrict_deprecation_warning if options[:dependent] == :restrict - send("define_#{options[:dependent]}_dependency_method") - model.before_destroy dependency_method_name - end - end - - def define_destroy_dependency_method - name = self.name - mixin.redefine_method(dependency_method_name) do - association(name).delete - end - end - alias :define_delete_dependency_method :define_destroy_dependency_method - alias :define_nullify_dependency_method :define_destroy_dependency_method - - def dependency_method_name - "has_one_dependent_#{options[:dependent]}_for_#{name}" - end + def valid_dependent_options + [:destroy, :delete, :nullify, :restrict, :restrict_with_error, :restrict_with_exception] + 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 index 436b6c1524..6a5830e57f 100644 --- a/activerecord/lib/active_record/associations/builder/singular_association.rb +++ b/activerecord/lib/active_record/associations/builder/singular_association.rb @@ -1,6 +1,8 @@ module ActiveRecord::Associations::Builder class SingularAssociation < Association #:nodoc: - self.valid_options += [:remote, :dependent, :counter_cache, :primary_key, :inverse_of] + def valid_options + super + [:remote, :dependent, :counter_cache, :primary_key, :inverse_of] + end def constructable? true @@ -11,22 +13,20 @@ module ActiveRecord::Associations::Builder define_constructors if constructable? end - private - - def define_constructors - name = self.name - - mixin.redefine_method("build_#{name}") do |*params, &block| - association(name).build(*params, &block) + def define_constructors + mixin.class_eval <<-CODE, __FILE__, __LINE__ + 1 + def build_#{name}(*args, &block) + association(:#{name}).build(*args, &block) end - mixin.redefine_method("create_#{name}") do |*params, &block| - association(name).create(*params, &block) + def create_#{name}(*args, &block) + association(:#{name}).create(*args, &block) end - mixin.redefine_method("create_#{name}!") do |*params, &block| - association(name).create!(*params, &block) + def create_#{name}!(*args, &block) + association(:#{name}).create!(*args, &block) end - end + CODE + end end end diff --git a/activerecord/lib/active_record/associations/collection_association.rb b/activerecord/lib/active_record/associations/collection_association.rb index b2136605e1..fe3e5b00f7 100644 --- a/activerecord/lib/active_record/associations/collection_association.rb +++ b/activerecord/lib/active_record/associations/collection_association.rb @@ -6,6 +6,15 @@ module ActiveRecord # ease the implementation of association proxies that represent # collections. See the class hierarchy in AssociationProxy. # + # CollectionAssociation: + # HasAndBelongsToManyAssociation => has_and_belongs_to_many + # HasManyAssociation => has_many + # HasManyThroughAssociation + ThroughAssociation => has_many :through + # + # CollectionAssociation class provides common methods to the collections + # defined by +has_and_belongs_to_many+, +has_many+ or +has_many+ with + # +:through association+ option. + # # 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 @@ -16,12 +25,6 @@ module ActiveRecord # 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) - super - @proxy = CollectionProxy.new(self) - end # Implements the reader method, e.g. foo.items for Foo.has_many :items def reader(force_reload = false) @@ -31,7 +34,7 @@ module ActiveRecord reload end - proxy + CollectionProxy.new(self) end # Implements the writer method, e.g. foo.items= for Foo.has_many :items @@ -47,18 +50,7 @@ module ActiveRecord end else column = "#{reflection.quoted_table_name}.#{reflection.association_primary_key}" - relation = scoped - - including = (relation.eager_load_values + relation.includes_values).uniq - - if including.any? - join_dependency = ActiveRecord::Associations::JoinDependency.new(reflection.klass, including, []) - relation = join_dependency.join_associations.inject(relation) do |r, association| - association.join_relation(r) - end - end - - relation.pluck(column) + scope.pluck(column) end end @@ -71,7 +63,7 @@ module ActiveRecord end def reset - @loaded = false + super @target = [] end @@ -79,7 +71,7 @@ module ActiveRecord if block_given? load_target.select.each { |e| yield e } else - scoped.select(select) + scope.select(select) end end @@ -90,7 +82,7 @@ module ActiveRecord if options[:finder_sql] find_by_scan(*args) else - scoped.find(*args) + scope.find(*args) end end end @@ -103,26 +95,27 @@ module ActiveRecord first_or_last(:last, *args) end - def build(attributes = {}, options = {}, &block) + def build(attributes = {}, &block) if attributes.is_a?(Array) - attributes.collect { |attr| build(attr, options, &block) } + attributes.collect { |attr| build(attr, &block) } else - add_to_target(build_record(attributes, options)) do |record| + add_to_target(build_record(attributes)) do |record| yield(record) if block_given? end end end - def create(attributes = {}, options = {}, &block) - create_record(attributes, options, &block) + def create(attributes = {}, &block) + create_record(attributes, &block) end - def create!(attributes = {}, options = {}, &block) - create_record(attributes, options, true, &block) + def create!(attributes = {}, &block) + create_record(attributes, true, &block) 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. + # 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) load_target if owner.new_record? @@ -148,23 +141,16 @@ module ActiveRecord end end - # Remove all records from this association + # Remove all records from this association. # # See delete for more info. def delete_all - delete(load_target).tap do + delete(:all).tap do reset loaded! end end - # Called when the association is declared as :dependent => :delete_all. This is - # an optimised version which avoids loading the records into memory. Not really - # for public consumption. - def delete_all_on_destroy - scoped.delete_all - end - # Destroy all the records from this association. # # See destroy for more info. @@ -175,12 +161,12 @@ module ActiveRecord end end - # Calculate sum using SQL, not Enumerable + # Calculate sum using SQL, not Enumerable. def sum(*args) if block_given? - scoped.sum(*args) { |*block_args| yield(*block_args) } + scope.sum(*args) { |*block_args| yield(*block_args) } else - scoped.sum(*args) + scope.sum(*args) end end @@ -197,13 +183,13 @@ module ActiveRecord reflection.klass.count_by_sql(custom_counter_sql) else - if options[:uniq] + if association_scope.uniq_value # This is needed because 'SELECT count(DISTINCT *)..' is not valid SQL. column_name ||= reflection.klass.primary_key - count_options.merge!(:distinct => true) + count_options[:distinct] = true end - value = scoped.count(column_name, count_options) + value = scope.count(column_name, count_options) limit = options[:limit] offset = options[:offset] @@ -224,7 +210,18 @@ module ActiveRecord # are actually removed from the database, that depends precisely on # +delete_records+. They are in any case removed from the collection. def delete(*records) - delete_or_destroy(records, options[:dependent]) + dependent = options[:dependent] + + if records.first == :all + if loaded? || dependent == :destroy + delete_or_destroy(load_target, dependent) + else + delete_records(:all, dependent) + end + else + records = find(records) if records.any? { |record| record.kind_of?(Fixnum) || record.kind_of?(String) } + delete_or_destroy(records, dependent) + end end # Destroy +records+ and remove them from this association calling @@ -248,11 +245,15 @@ module ActiveRecord # This method is abstract in the sense that it relies on # +count_records+, which is a method descendants have to provide. def size - if !find_target? || (loaded? && !options[:uniq]) - target.size - elsif !loaded? && options[:group] + if !find_target? || loaded? + if association_scope.uniq_value + target.uniq.size + else + target.size + end + elsif !loaded? && !association_scope.group_values.empty? load_target.size - elsif !loaded? && !options[:uniq] && target.is_a?(Array) + elsif !loaded? && !association_scope.uniq_value && target.is_a?(Array) unsaved_records = target.select { |r| r.new_record? } unsaved_records.size + count_records else @@ -269,13 +270,24 @@ module ActiveRecord 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>. + # Returns true if the collection is empty. + # + # If the collection has been loaded or the <tt>:counter_sql</tt> option + # is provided, it is equivalent to <tt>collection.size.zero?</tt>. If the + # collection has not been loaded, it is equivalent to + # <tt>collection.exists?</tt>. If the collection has not already been + # loaded and you are going to fetch the records anyway it is better to + # check <tt>collection.length.zero?</tt>. def empty? - size.zero? + if loaded? || options[:counter_sql] + size.zero? + else + !scope.exists? + end end + # Returns true if the collections is not empty. + # Equivalent to +!collection.empty?+. def any? if block_given? load_target.any? { |*block_args| yield(*block_args) } @@ -284,7 +296,8 @@ module ActiveRecord end end - # Returns true if the collection has more than 1 record. Equivalent to collection.size > 1. + # 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) } @@ -293,15 +306,15 @@ module ActiveRecord end end - def uniq(collection = load_target) + def uniq seen = {} - collection.find_all do |record| + load_target.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. + # 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 @@ -319,7 +332,7 @@ module ActiveRecord include_in_memory?(record) else load_target if options[:finder_sql] - loaded? ? target.include?(record) : scoped.exists?(record) + loaded? ? target.include?(record) : scope.exists?(record) end else false @@ -339,7 +352,7 @@ module ActiveRecord callback(:before_add, record) yield(record) if block_given? - if options[:uniq] && index = @target.index(record) + if association_scope.uniq_value && index = @target.index(record) @target[index] = record else @target << record @@ -360,7 +373,7 @@ module ActiveRecord # replace the SELECT clause with COUNT(SELECTS), preserving any hints within /* ... */ interpolate(options[:finder_sql]).sub(/SELECT\b(\/\*.*?\*\/ )?(.*)\bFROM\b/im) do count_with = $2.to_s - count_with = '*' if count_with.blank? || count_with =~ /,/ + count_with = '*' if count_with.blank? || count_with =~ /,/ || count_with =~ /\.\*/ "SELECT #{$1}COUNT(#{count_with}) FROM" end end @@ -375,10 +388,9 @@ module ActiveRecord if options[:finder_sql] reflection.klass.find_by_sql(custom_finder_sql) else - scoped.all + scope.to_a end - records = options[:uniq] ? uniq(records) : records records.each { |record| set_inverse_instance(record) } records end @@ -413,16 +425,16 @@ module ActiveRecord persisted + memory end - def create_record(attributes, options, raise = false, &block) + def create_record(attributes, raise = false, &block) unless owner.persisted? raise ActiveRecord::RecordNotSaved, "You cannot call create unless the parent is saved" end if attributes.is_a?(Array) - attributes.collect { |attr| create_record(attr, options, raise, &block) } + attributes.collect { |attr| create_record(attr, raise, &block) } else transaction do - add_to_target(build_record(attributes, options)) do |record| + add_to_target(build_record(attributes)) do |record| yield(record) if block_given? insert_record(record, true, raise) end @@ -436,7 +448,7 @@ module ActiveRecord end def create_scope - scoped.scope_for_create.stringify_keys + scope.scope_for_create.stringify_keys end def delete_or_destroy(records, method) @@ -474,6 +486,8 @@ module ActiveRecord raise RecordNotSaved, "Failed to replace #{reflection.name} because one or more of the " \ "new records could not be saved." end + + target end def concat_records(records) @@ -544,7 +558,7 @@ module ActiveRecord # 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 } + ids = args.flatten.compact.map{ |arg| arg.to_i }.uniq if ids.size == 1 id = ids.first @@ -559,8 +573,8 @@ module ActiveRecord 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) + collection = fetch_first_or_last_using_find?(args) ? scope : load_target + collection.send(type, *args).tap {|it| set_inverse_instance it } end end end diff --git a/activerecord/lib/active_record/associations/collection_proxy.rb b/activerecord/lib/active_record/associations/collection_proxy.rb index 5eda0387c4..c113957faa 100644 --- a/activerecord/lib/active_record/associations/collection_proxy.rb +++ b/activerecord/lib/active_record/associations/collection_proxy.rb @@ -18,14 +18,8 @@ module ActiveRecord # <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. + # This class delegates unknown methods to <tt>@target</tt> via + # <tt>method_missing</tt>. # # The <tt>@target</tt> object is not \loaded until needed. For example, # @@ -33,26 +27,811 @@ module ActiveRecord # # 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 + class CollectionProxy < Relation + def initialize(association) #:nodoc: + @association = association + super association.klass, association.klass.arel_table + merge! association.scope + end - instance_methods.each { |m| undef_method m unless m.to_s =~ /^(?:nil\?|send|object_id|to_a)$|^__|^respond_to|proxy_/ } + def target + @association.target + end - delegate :group, :order, :limit, :joins, :where, :preload, :eager_load, :includes, :from, - :lock, :readonly, :having, :pluck, :to => :scoped + def load_target + @association.load_target + end - delegate :target, :load_target, :loaded?, :to => :@association + def loaded? + @association.loaded? + end - delegate :select, :find, :first, :last, - :build, :create, :create!, - :concat, :replace, :delete_all, :destroy_all, :delete, :destroy, :uniq, - :sum, :count, :size, :length, :empty?, - :any?, :many?, :include?, - :to => :@association + ## + # Works in two ways. + # + # *First:* Specify a subset of fields to be selected from the result set. + # + # class Person < ActiveRecord::Base + # has_many :pets + # end + # + # person.pets + # # => [ + # # #<Pet id: 1, name: "Fancy-Fancy", person_id: 1>, + # # #<Pet id: 2, name: "Spook", person_id: 1>, + # # #<Pet id: 3, name: "Choo-Choo", person_id: 1> + # # ] + # + # person.pets.select(:name) + # # => [ + # # #<Pet id: nil, name: "Fancy-Fancy">, + # # #<Pet id: nil, name: "Spook">, + # # #<Pet id: nil, name: "Choo-Choo"> + # # ] + # + # person.pets.select([:id, :name]) + # # => [ + # # #<Pet id: 1, name: "Fancy-Fancy">, + # # #<Pet id: 2, name: "Spook">, + # # #<Pet id: 3, name: "Choo-Choo"> + # # ] + # + # Be careful because this also means you’re initializing a model + # object with only the fields that you’ve selected. If you attempt + # to access a field that is not in the initialized record you’ll + # receive: + # + # person.pets.select(:name).first.person_id + # # => ActiveModel::MissingAttributeError: missing attribute: person_id + # + # *Second:* You can pass a block so it can be used just like Array#select. + # This build an array of objects from the database for the scope, + # converting them into an array and iterating through them using + # Array#select. + # + # person.pets.select { |pet| pet.name =~ /oo/ } + # # => [ + # # #<Pet id: 2, name: "Spook", person_id: 1>, + # # #<Pet id: 3, name: "Choo-Choo", person_id: 1> + # # ] + # + # person.pets.select(:name) { |pet| pet.name =~ /oo/ } + # # => [ + # # #<Pet id: 2, name: "Spook">, + # # #<Pet id: 3, name: "Choo-Choo"> + # # ] + def select(select = nil, &block) + @association.select(select, &block) + end - def initialize(association) - @association = association - Array(association.options[:extend]).each { |ext| proxy_extend(ext) } + ## + # Finds an object in the collection responding to the +id+. Uses the same + # rules as +ActiveRecord::Base.find+. Returns +ActiveRecord::RecordNotFound++ + # error if the object can not be found. + # + # class Person < ActiveRecord::Base + # has_many :pets + # end + # + # person.pets + # # => [ + # # #<Pet id: 1, name: "Fancy-Fancy", person_id: 1>, + # # #<Pet id: 2, name: "Spook", person_id: 1>, + # # #<Pet id: 3, name: "Choo-Choo", person_id: 1> + # # ] + # + # person.pets.find(1) # => #<Pet id: 1, name: "Fancy-Fancy", person_id: 1> + # person.pets.find(4) # => ActiveRecord::RecordNotFound: Couldn't find Pet with id=4 + # + # person.pets.find(2) { |pet| pet.name.downcase! } + # # => #<Pet id: 2, name: "fancy-fancy", person_id: 1> + # + # person.pets.find(2, 3) + # # => [ + # # #<Pet id: 2, name: "Spook", person_id: 1>, + # # #<Pet id: 3, name: "Choo-Choo", person_id: 1> + # # ] + def find(*args, &block) + @association.find(*args, &block) + end + + ## + # Returns the first record, or the first +n+ records, from the collection. + # If the collection is empty, the first form returns +nil+, and the second + # form returns an empty array. + # + # class Person < ActiveRecord::Base + # has_many :pets + # end + # + # person.pets + # # => [ + # # #<Pet id: 1, name: "Fancy-Fancy", person_id: 1>, + # # #<Pet id: 2, name: "Spook", person_id: 1>, + # # #<Pet id: 3, name: "Choo-Choo", person_id: 1> + # # ] + # + # person.pets.first # => #<Pet id: 1, name: "Fancy-Fancy", person_id: 1> + # + # person.pets.first(2) + # # => [ + # # #<Pet id: 1, name: "Fancy-Fancy", person_id: 1>, + # # #<Pet id: 2, name: "Spook", person_id: 1> + # # ] + # + # another_person_without.pets # => [] + # another_person_without.pets.first # => nil + # another_person_without.pets.first(3) # => [] + def first(*args) + @association.first(*args) + end + + ## + # Returns the last record, or the last +n+ records, from the collection. + # If the collection is empty, the first form returns +nil+, and the second + # form returns an empty array. + # + # class Person < ActiveRecord::Base + # has_many :pets + # end + # + # person.pets + # # => [ + # # #<Pet id: 1, name: "Fancy-Fancy", person_id: 1>, + # # #<Pet id: 2, name: "Spook", person_id: 1>, + # # #<Pet id: 3, name: "Choo-Choo", person_id: 1> + # # ] + # + # person.pets.last # => #<Pet id: 3, name: "Choo-Choo", person_id: 1> + # + # person.pets.last(2) + # # => [ + # # #<Pet id: 2, name: "Spook", person_id: 1>, + # # #<Pet id: 3, name: "Choo-Choo", person_id: 1> + # # ] + # + # another_person_without.pets # => [] + # another_person_without.pets.last # => nil + # another_person_without.pets.last(3) # => [] + def last(*args) + @association.last(*args) + end + + ## + # Returns a new object of the collection type that has been instantiated + # with +attributes+ and linked to this object, but have not yet been saved. + # You can pass an array of attributes hashes, this will return an array + # with the new objects. + # + # class Person + # has_many :pets + # end + # + # person.pets.build + # # => #<Pet id: nil, name: nil, person_id: 1> + # + # person.pets.build(name: 'Fancy-Fancy') + # # => #<Pet id: nil, name: "Fancy-Fancy", person_id: 1> + # + # person.pets.build([{name: 'Spook'}, {name: 'Choo-Choo'}, {name: 'Brain'}]) + # # => [ + # # #<Pet id: nil, name: "Spook", person_id: 1>, + # # #<Pet id: nil, name: "Choo-Choo", person_id: 1>, + # # #<Pet id: nil, name: "Brain", person_id: 1> + # # ] + # + # person.pets.size # => 5 # size of the collection + # person.pets.count # => 0 # count from database + def build(attributes = {}, &block) + @association.build(attributes, &block) + end + + ## + # Returns a new object of the collection type that has been instantiated with + # attributes, linked to this object and that has already been saved (if it + # passes the validations). + # + # class Person + # has_many :pets + # end + # + # person.pets.create(name: 'Fancy-Fancy') + # # => #<Pet id: 1, name: "Fancy-Fancy", person_id: 1> + # + # person.pets.create([{name: 'Spook'}, {name: 'Choo-Choo'}]) + # # => [ + # # #<Pet id: 2, name: "Spook", person_id: 1>, + # # #<Pet id: 3, name: "Choo-Choo", person_id: 1> + # # ] + # + # person.pets.size # => 3 + # person.pets.count # => 3 + # + # person.pets.find(1, 2, 3) + # # => [ + # # #<Pet id: 1, name: "Fancy-Fancy", person_id: 1>, + # # #<Pet id: 2, name: "Spook", person_id: 1>, + # # #<Pet id: 3, name: "Choo-Choo", person_id: 1> + # # ] + def create(attributes = {}, &block) + @association.create(attributes, &block) + end + + ## + # Like +create+, except that if the record is invalid, raises an exception. + # + # class Person + # has_many :pets + # end + # + # class Pet + # validates :name, presence: true + # end + # + # person.pets.create!(name: nil) + # # => ActiveRecord::RecordInvalid: Validation failed: Name can't be blank + def create!(attributes = {}, &block) + @association.create!(attributes, &block) + end + + ## + # Add one or more records to the collection by setting their foreign keys + # to the association's primary key. Since << flattens its argument list and + # inserts each record, +push+ and +concat+ behave identically. Returns +self+ + # so method calls may be chained. + # + # class Person < ActiveRecord::Base + # pets :has_many + # end + # + # person.pets.size # => 0 + # person.pets.concat(Pet.new(name: 'Fancy-Fancy')) + # person.pets.concat(Pet.new(name: 'Spook'), Pet.new(name: 'Choo-Choo')) + # person.pets.size # => 3 + # + # person.id # => 1 + # person.pets + # # => [ + # # #<Pet id: 1, name: "Fancy-Fancy", person_id: 1>, + # # #<Pet id: 2, name: "Spook", person_id: 1>, + # # #<Pet id: 3, name: "Choo-Choo", person_id: 1> + # # ] + # + # person.pets.concat([Pet.new(name: 'Brain'), Pet.new(name: 'Benny')]) + # person.pets.size # => 5 + def concat(*records) + @association.concat(*records) + end + + ## + # Replace this collection with +other_array+. This will perform a diff + # and delete/add only records that have changed. + # + # class Person < ActiveRecord::Base + # has_many :pets + # end + # + # person.pets + # # => [#<Pet id: 1, name: "Gorby", group: "cats", person_id: 1>] + # + # other_pets = [Pet.new(name: 'Puff', group: 'celebrities'] + # + # person.pets.replace(other_pets) + # + # person.pets + # # => [#<Pet id: 2, name: "Puff", group: "celebrities", person_id: 1>] + # + # If the supplied array has an incorrect association type, it raises + # an <tt>ActiveRecord::AssociationTypeMismatch</tt> error: + # + # person.pets.replace(["doo", "ggie", "gaga"]) + # # => ActiveRecord::AssociationTypeMismatch: Pet expected, got String + def replace(other_array) + @association.replace(other_array) + end + + ## + # Deletes all the records from the collection. For +has_many+ associations, + # the deletion is done according to the strategy specified by the <tt>:dependent</tt> + # option. Returns an array with the deleted records. + # + # If no <tt>:dependent</tt> option is given, then it will follow the + # default strategy. The default strategy is <tt>:nullify</tt>. This + # sets the foreign keys to <tt>NULL</tt>. For, +has_many+ <tt>:through</tt>, + # the default strategy is +delete_all+. + # + # class Person < ActiveRecord::Base + # has_many :pets # dependent: :nullify option by default + # end + # + # person.pets.size # => 3 + # person.pets + # # => [ + # # #<Pet id: 1, name: "Fancy-Fancy", person_id: 1>, + # # #<Pet id: 2, name: "Spook", person_id: 1>, + # # #<Pet id: 3, name: "Choo-Choo", person_id: 1> + # # ] + # + # person.pets.delete_all + # # => [ + # # #<Pet id: 1, name: "Fancy-Fancy", person_id: 1>, + # # #<Pet id: 2, name: "Spook", person_id: 1>, + # # #<Pet id: 3, name: "Choo-Choo", person_id: 1> + # # ] + # + # person.pets.size # => 0 + # person.pets # => [] + # + # Pet.find(1, 2, 3) + # # => [ + # # #<Pet id: 1, name: "Fancy-Fancy", person_id: nil>, + # # #<Pet id: 2, name: "Spook", person_id: nil>, + # # #<Pet id: 3, name: "Choo-Choo", person_id: nil> + # # ] + # + # If it is set to <tt>:destroy</tt> all the objects from the collection + # are removed by calling their +destroy+ method. See +destroy+ for more + # information. + # + # class Person < ActiveRecord::Base + # has_many :pets, dependent: :destroy + # end + # + # person.pets.size # => 3 + # person.pets + # # => [ + # # #<Pet id: 1, name: "Fancy-Fancy", person_id: 1>, + # # #<Pet id: 2, name: "Spook", person_id: 1>, + # # #<Pet id: 3, name: "Choo-Choo", person_id: 1> + # # ] + # + # person.pets.delete_all + # # => [ + # # #<Pet id: 1, name: "Fancy-Fancy", person_id: 1>, + # # #<Pet id: 2, name: "Spook", person_id: 1>, + # # #<Pet id: 3, name: "Choo-Choo", person_id: 1> + # # ] + # + # Pet.find(1, 2, 3) + # # => ActiveRecord::RecordNotFound + # + # If it is set to <tt>:delete_all</tt>, all the objects are deleted + # *without* calling their +destroy+ method. + # + # class Person < ActiveRecord::Base + # has_many :pets, dependent: :delete_all + # end + # + # person.pets.size # => 3 + # person.pets + # # => [ + # # #<Pet id: 1, name: "Fancy-Fancy", person_id: 1>, + # # #<Pet id: 2, name: "Spook", person_id: 1>, + # # #<Pet id: 3, name: "Choo-Choo", person_id: 1> + # # ] + # + # person.pets.delete_all + # # => [ + # # #<Pet id: 1, name: "Fancy-Fancy", person_id: 1>, + # # #<Pet id: 2, name: "Spook", person_id: 1>, + # # #<Pet id: 3, name: "Choo-Choo", person_id: 1> + # # ] + # + # Pet.find(1, 2, 3) + # # => ActiveRecord::RecordNotFound + def delete_all + @association.delete_all + end + + ## + # Deletes the records of the collection directly from the database. + # This will _always_ remove the records ignoring the +:dependent+ + # option. + # + # class Person < ActiveRecord::Base + # has_many :pets + # end + # + # person.pets.size # => 3 + # person.pets + # # => [ + # # #<Pet id: 1, name: "Fancy-Fancy", person_id: 1>, + # # #<Pet id: 2, name: "Spook", person_id: 1>, + # # #<Pet id: 3, name: "Choo-Choo", person_id: 1> + # # ] + # + # person.pets.destroy_all + # + # person.pets.size # => 0 + # person.pets # => [] + # + # Pet.find(1) # => Couldn't find Pet with id=1 + def destroy_all + @association.destroy_all + end + + ## + # Deletes the +records+ supplied and removes them from the collection. For + # +has_many+ associations, the deletion is done according to the strategy + # specified by the <tt>:dependent</tt> option. Returns an array with the + # deleted records. + # + # If no <tt>:dependent</tt> option is given, then it will follow the default + # strategy. The default strategy is <tt>:nullify</tt>. This sets the foreign + # keys to <tt>NULL</tt>. For, +has_many+ <tt>:through</tt>, the default + # strategy is +delete_all+. + # + # class Person < ActiveRecord::Base + # has_many :pets # dependent: :nullify option by default + # end + # + # person.pets.size # => 3 + # person.pets + # # => [ + # # #<Pet id: 1, name: "Fancy-Fancy", person_id: 1>, + # # #<Pet id: 2, name: "Spook", person_id: 1>, + # # #<Pet id: 3, name: "Choo-Choo", person_id: 1> + # # ] + # + # person.pets.delete(Pet.find(1)) + # # => [#<Pet id: 1, name: "Fancy-Fancy", person_id: 1>] + # + # person.pets.size # => 2 + # person.pets + # # => [ + # # #<Pet id: 2, name: "Spook", person_id: 1>, + # # #<Pet id: 3, name: "Choo-Choo", person_id: 1> + # # ] + # + # Pet.find(1) + # # => #<Pet id: 1, name: "Fancy-Fancy", person_id: nil> + # + # If it is set to <tt>:destroy</tt> all the +records+ are removed by calling + # their +destroy+ method. See +destroy+ for more information. + # + # class Person < ActiveRecord::Base + # has_many :pets, dependent: :destroy + # end + # + # person.pets.size # => 3 + # person.pets + # # => [ + # # #<Pet id: 1, name: "Fancy-Fancy", person_id: 1>, + # # #<Pet id: 2, name: "Spook", person_id: 1>, + # # #<Pet id: 3, name: "Choo-Choo", person_id: 1> + # # ] + # + # person.pets.delete(Pet.find(1), Pet.find(3)) + # # => [ + # # #<Pet id: 1, name: "Fancy-Fancy", person_id: 1>, + # # #<Pet id: 3, name: "Choo-Choo", person_id: 1> + # # ] + # + # person.pets.size # => 1 + # person.pets + # # => [#<Pet id: 2, name: "Spook", person_id: 1>] + # + # Pet.find(1, 3) + # # => ActiveRecord::RecordNotFound: Couldn't find all Pets with IDs (1, 3) + # + # If it is set to <tt>:delete_all</tt>, all the +records+ are deleted + # *without* calling their +destroy+ method. + # + # class Person < ActiveRecord::Base + # has_many :pets, dependent: :delete_all + # end + # + # person.pets.size # => 3 + # person.pets + # # => [ + # # #<Pet id: 1, name: "Fancy-Fancy", person_id: 1>, + # # #<Pet id: 2, name: "Spook", person_id: 1>, + # # #<Pet id: 3, name: "Choo-Choo", person_id: 1> + # # ] + # + # person.pets.delete(Pet.find(1)) + # # => [#<Pet id: 1, name: "Fancy-Fancy", person_id: 1>] + # + # person.pets.size # => 2 + # person.pets + # # => [ + # # #<Pet id: 2, name: "Spook", person_id: 1>, + # # #<Pet id: 3, name: "Choo-Choo", person_id: 1> + # # ] + # + # Pet.find(1) + # # => ActiveRecord::RecordNotFound: Couldn't find Pet with id=1 + # + # You can pass +Fixnum+ or +String+ values, it finds the records + # responding to the +id+ and executes delete on them. + # + # class Person < ActiveRecord::Base + # has_many :pets + # end + # + # person.pets.size # => 3 + # person.pets + # # => [ + # # #<Pet id: 1, name: "Fancy-Fancy", person_id: 1>, + # # #<Pet id: 2, name: "Spook", person_id: 1>, + # # #<Pet id: 3, name: "Choo-Choo", person_id: 1> + # # ] + # + # person.pets.delete("1") + # # => [#<Pet id: 1, name: "Fancy-Fancy", person_id: 1>] + # + # person.pets.delete(2, 3) + # # => [ + # # #<Pet id: 2, name: "Spook", person_id: 1>, + # # #<Pet id: 3, name: "Choo-Choo", person_id: 1> + # # ] + def delete(*records) + @association.delete(*records) + end + + ## + # Destroys the +records+ supplied and removes them from the collection. + # This method will _always_ remove record from the database ignoring + # the +:dependent+ option. Returns an array with the removed records. + # + # class Person < ActiveRecord::Base + # has_many :pets + # end + # + # person.pets.size # => 3 + # person.pets + # # => [ + # # #<Pet id: 1, name: "Fancy-Fancy", person_id: 1>, + # # #<Pet id: 2, name: "Spook", person_id: 1>, + # # #<Pet id: 3, name: "Choo-Choo", person_id: 1> + # # ] + # + # person.pets.destroy(Pet.find(1)) + # # => [#<Pet id: 1, name: "Fancy-Fancy", person_id: 1>] + # + # person.pets.size # => 2 + # person.pets + # # => [ + # # #<Pet id: 2, name: "Spook", person_id: 1>, + # # #<Pet id: 3, name: "Choo-Choo", person_id: 1> + # # ] + # + # person.pets.destroy(Pet.find(2), Pet.find(3)) + # # => [ + # # #<Pet id: 2, name: "Spook", person_id: 1>, + # # #<Pet id: 3, name: "Choo-Choo", person_id: 1> + # # ] + # + # person.pets.size # => 0 + # person.pets # => [] + # + # Pet.find(1, 2, 3) # => ActiveRecord::RecordNotFound: Couldn't find all Pets with IDs (1, 2, 3) + # + # You can pass +Fixnum+ or +String+ values, it finds the records + # responding to the +id+ and then deletes them from the database. + # + # person.pets.size # => 3 + # person.pets + # # => [ + # # #<Pet id: 4, name: "Benny", person_id: 1>, + # # #<Pet id: 5, name: "Brain", person_id: 1>, + # # #<Pet id: 6, name: "Boss", person_id: 1> + # # ] + # + # person.pets.destroy("4") + # # => #<Pet id: 4, name: "Benny", person_id: 1> + # + # person.pets.size # => 2 + # person.pets + # # => [ + # # #<Pet id: 5, name: "Brain", person_id: 1>, + # # #<Pet id: 6, name: "Boss", person_id: 1> + # # ] + # + # person.pets.destroy(5, 6) + # # => [ + # # #<Pet id: 5, name: "Brain", person_id: 1>, + # # #<Pet id: 6, name: "Boss", person_id: 1> + # # ] + # + # person.pets.size # => 0 + # person.pets # => [] + # + # Pet.find(4, 5, 6) # => ActiveRecord::RecordNotFound: Couldn't find all Pets with IDs (4, 5, 6) + def destroy(*records) + @association.destroy(*records) + end + + ## + # Specifies whether the records should be unique or not. + # + # class Person < ActiveRecord::Base + # has_many :pets + # end + # + # person.pets.select(:name) + # # => [ + # # #<Pet name: "Fancy-Fancy">, + # # #<Pet name: "Fancy-Fancy"> + # # ] + # + # person.pets.select(:name).uniq + # # => [#<Pet name: "Fancy-Fancy">] + def uniq + @association.uniq + end + + ## + # Count all records using SQL. + # + # class Person < ActiveRecord::Base + # has_many :pets + # end + # + # person.pets.count # => 3 + # person.pets + # # => [ + # # #<Pet id: 1, name: "Fancy-Fancy", person_id: 1>, + # # #<Pet id: 2, name: "Spook", person_id: 1>, + # # #<Pet id: 3, name: "Choo-Choo", person_id: 1> + # # ] + def count(column_name = nil, options = {}) + @association.count(column_name, options) + end + + ## + # Returns the size of the collection. If the collection hasn't been loaded, + # it executes a <tt>SELECT COUNT(*)</tt> query. + # + # class Person < ActiveRecord::Base + # has_many :pets + # end + # + # person.pets.size # => 3 + # # executes something like SELECT COUNT(*) FROM "pets" WHERE "pets"."person_id" = 1 + # + # person.pets # This will execute a SELECT * FROM query + # # => [ + # # #<Pet id: 1, name: "Fancy-Fancy", person_id: 1>, + # # #<Pet id: 2, name: "Spook", person_id: 1>, + # # #<Pet id: 3, name: "Choo-Choo", person_id: 1> + # # ] + # + # person.pets.size # => 3 + # # Because the collection is already loaded, this will behave like + # # collection.size and no SQL count query is executed. + def size + @association.size + end + + ## + # Returns the size of the collection calling +size+ on the target. + # If the collection has been already loaded, +length+ and +size+ are + # equivalent. + # + # class Person < ActiveRecord::Base + # has_many :pets + # end + # + # person.pets.length # => 3 + # # executes something like SELECT "pets".* FROM "pets" WHERE "pets"."person_id" = 1 + # + # # Because the collection is loaded, you can + # # call the collection with no additional queries: + # person.pets + # # => [ + # # #<Pet id: 1, name: "Fancy-Fancy", person_id: 1>, + # # #<Pet id: 2, name: "Spook", person_id: 1>, + # # #<Pet id: 3, name: "Choo-Choo", person_id: 1> + # # ] + def length + @association.length + end + + ## + # Returns +true+ if the collection is empty. + # + # class Person < ActiveRecord::Base + # has_many :pets + # end + # + # person.pets.count # => 1 + # person.pets.empty? # => false + # + # person.pets.delete_all + # + # person.pets.count # => 0 + # person.pets.empty? # => true + def empty? + @association.empty? + end + + ## + # Returns +true+ if the collection is not empty. + # + # class Person < ActiveRecord::Base + # has_many :pets + # end + # + # person.pets.count # => 0 + # person.pets.any? # => false + # + # person.pets << Pet.new(name: 'Snoop') + # person.pets.count # => 0 + # person.pets.any? # => true + # + # You can also pass a block to define criteria. The behaviour + # is the same, it returns true if the collection based on the + # criteria is not empty. + # + # person.pets + # # => [#<Pet name: "Snoop", group: "dogs">] + # + # person.pets.any? do |pet| + # pet.group == 'cats' + # end + # # => false + # + # person.pets.any? do |pet| + # pet.group == 'dogs' + # end + # # => true + def any?(&block) + @association.any?(&block) + end + + ## + # Returns true if the collection has more than one record. + # Equivalent to <tt>collection.size > 1</tt>. + # + # class Person < ActiveRecord::Base + # has_many :pets + # end + # + # person.pets.count #=> 1 + # person.pets.many? #=> false + # + # person.pets << Pet.new(name: 'Snoopy') + # person.pets.count #=> 2 + # person.pets.many? #=> true + # + # You can also pass a block to define criteria. The + # behaviour is the same, it returns true if the collection + # based on the criteria has more than one record. + # + # person.pets + # # => [ + # # #<Pet name: "Gorby", group: "cats">, + # # #<Pet name: "Puff", group: "cats">, + # # #<Pet name: "Snoop", group: "dogs"> + # # ] + # + # person.pets.many? do |pet| + # pet.group == 'dogs' + # end + # # => false + # + # person.pets.many? do |pet| + # pet.group == 'cats' + # end + # # => true + def many?(&block) + @association.many?(&block) + end + + ## + # Returns +true+ if the given object is present in the collection. + # + # class Person < ActiveRecord::Base + # has_many :pets + # end + # + # person.pets # => [#<Pet id: 20, name: "Snoop">] + # + # person.pets.include?(Pet.find(20)) # => true + # person.pets.include?(Pet.find(21)) # => false + def include?(record) + @association.include?(record) end alias_method :new, :build @@ -61,67 +840,143 @@ module ActiveRecord @association end - def scoped + # We don't want this object to be put on the scoping stack, because + # that could create an infinite loop where we call an @association + # method, which gets the current scope, which is this object, which + # delegates to @association, and so on. + def scoping + @association.scope.scoping { yield } + end + + # Returns a <tt>Relation</tt> object for the records in this association + def scope association = @association - association.scoped.extending do + + @association.scope.extending! do define_method(:proxy_association) { association } end end - def respond_to?(name, include_private = false) - super || - (load_target && target.respond_to?(name, include_private)) || - proxy_association.klass.respond_to?(name, include_private) - end - - def method_missing(method, *args, &block) - match = DynamicFinderMatch.match(method) - if match && match.instantiator? - send(:find_or_instantiator_by_attributes, match, match.attribute_names, *args) do |r| - proxy_association.send :set_owner_attributes, r - proxy_association.send :add_to_target, r - yield(r) if block_given? - end - - elsif target.respond_to?(method) || (!proxy_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 - - else - scoped.readonly(nil).send(method, *args, &block) - end - end + # :nodoc: + alias spawn scope - # 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 + # Equivalent to <tt>Array#==</tt>. Returns +true+ if the two arrays + # contain the same number of elements and if each element is equal + # to the corresponding element in the other array, otherwise returns + # +false+. + # + # class Person < ActiveRecord::Base + # has_many :pets + # end + # + # person.pets + # # => [ + # # #<Pet id: 1, name: "Fancy-Fancy", person_id: 1>, + # # #<Pet id: 2, name: "Spook", person_id: 1> + # # ] + # + # other = person.pets.to_ary + # + # person.pets == other + # # => true + # + # other = [Pet.new(id: 1), Pet.new(id: 2)] + # + # person.pets == other + # # => false + def ==(other) + load_target == other end + # Returns a new array of objects from the collection. If the collection + # hasn't been loaded, it fetches the records from the database. + # + # class Person < ActiveRecord::Base + # has_many :pets + # end + # + # person.pets + # # => [ + # # #<Pet id: 4, name: "Benny", person_id: 1>, + # # #<Pet id: 5, name: "Brain", person_id: 1>, + # # #<Pet id: 6, name: "Boss", person_id: 1> + # # ] + # + # other_pets = person.pets.to_ary + # # => [ + # # #<Pet id: 4, name: "Benny", person_id: 1>, + # # #<Pet id: 5, name: "Brain", person_id: 1>, + # # #<Pet id: 6, name: "Boss", person_id: 1> + # # ] + # + # other_pets.replace([Pet.new(name: 'BooGoo')]) + # + # other_pets + # # => [#<Pet id: nil, name: "BooGoo", person_id: 1>] + # + # person.pets + # # This is not affected by replace + # # => [ + # # #<Pet id: 4, name: "Benny", person_id: 1>, + # # #<Pet id: 5, name: "Brain", person_id: 1>, + # # #<Pet id: 6, name: "Boss", person_id: 1> + # # ] def to_ary load_target.dup end alias_method :to_a, :to_ary + # Adds one or more +records+ to the collection by setting their foreign keys + # to the association‘s primary key. Returns +self+, so several appends may be + # chained together. + # + # class Person < ActiveRecord::Base + # has_many :pets + # end + # + # person.pets.size # => 0 + # person.pets << Pet.new(name: 'Fancy-Fancy') + # person.pets << [Pet.new(name: 'Spook'), Pet.new(name: 'Choo-Choo')] + # person.pets.size # => 3 + # + # person.id # => 1 + # person.pets + # # => [ + # # #<Pet id: 1, name: "Fancy-Fancy", person_id: 1>, + # # #<Pet id: 2, name: "Spook", person_id: 1>, + # # #<Pet id: 3, name: "Choo-Choo", person_id: 1> + # # ] def <<(*records) proxy_association.concat(records) && self end alias_method :push, :<< + # Equivalent to +delete_all+. The difference is that returns +self+, instead + # of an array with the deleted objects, so methods can be chained. See + # +delete_all+ for more information. def clear delete_all self end + # Reloads the collection from the database. Returns +self+. + # Equivalent to <tt>collection(true)</tt>. + # + # class Person < ActiveRecord::Base + # has_many :pets + # end + # + # person.pets # fetches pets from the database + # # => [#<Pet id: 1, name: "Snoop", group: "dogs", person_id: 1>] + # + # person.pets # uses the pets cache + # # => [#<Pet id: 1, name: "Snoop", group: "dogs", person_id: 1>] + # + # person.pets.reload # fetches pets from the database + # # => [#<Pet id: 1, name: "Snoop", group: "dogs", person_id: 1>] + # + # person.pets(true)  # fetches pets from the database + # # => [#<Pet id: 1, name: "Snoop", group: "dogs", person_id: 1>] def reload proxy_association.reload self 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 a4cea99372..93618721bb 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 @@ -5,7 +5,7 @@ module ActiveRecord attr_reader :join_table def initialize(owner, reflection) - @join_table = Arel::Table.new(reflection.options[:join_table]) + @join_table = Arel::Table.new(reflection.join_table) super end @@ -32,10 +32,6 @@ module ActiveRecord record end - # ActiveRecord::Relation#delete_all needs to support joins before we can use a - # SQL-only implementation. - alias delete_all_on_destroy delete_all - private def count_records @@ -44,13 +40,20 @@ module ActiveRecord def delete_records(records, method) if sql = options[:delete_sql] + records = load_target if records == :all records.each { |record| owner.connection.delete(interpolate(sql, record)) } else - 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 + relation = join_table + condition = relation[reflection.foreign_key].eq(owner.id) + + unless records == :all + condition = condition.and( + relation[reflection.association_foreign_key] + .in(records.map { |x| x.id }.compact) + ) + end + + owner.connection.delete(relation.where(condition).compile_delete) 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 059e6c77bc..74864d271f 100644 --- a/activerecord/lib/active_record/associations/has_many_association.rb +++ b/activerecord/lib/active_record/associations/has_many_association.rb @@ -7,6 +7,28 @@ module ActiveRecord # is provided by its child HasManyThroughAssociation. class HasManyAssociation < CollectionAssociation #:nodoc: + def handle_dependency + case options[:dependent] + when :restrict, :restrict_with_exception + raise ActiveRecord::DeleteRestrictionError.new(reflection.name) unless empty? + + when :restrict_with_error + unless empty? + record = klass.human_attribute_name(reflection.name).downcase + owner.errors.add(:base, :"restrict_dependent_destroy.many", record: record) + false + end + + else + if options[:dependent] == :destroy + # No point in executing the counter update since we're going to destroy the parent anyway + load_target.each(&:mark_for_destruction) + end + + delete_all + end + end + def insert_record(record, validate = true, raise = false) set_owner_attributes(record) @@ -38,7 +60,7 @@ module ActiveRecord elsif options[:counter_sql] || options[:finder_sql] reflection.klass.count_by_sql(custom_counter_sql) else - scoped.count + scope.count end # If there's nothing in the database and @target has no new records @@ -46,7 +68,7 @@ module ActiveRecord # documented side-effect of the method that may avoid an extra SELECT. @target ||= [] and loaded! if count == 0 - [options[:limit], count].compact.min + [association_scope.limit_value, count].compact.min end def has_cached_counter?(reflection = reflection) @@ -89,8 +111,12 @@ module ActiveRecord 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 records == :all + scope = self.scope + else + keys = records.map { |r| r[reflection.association_primary_key] } + scope = self.scope.where(reflection.association_primary_key => keys) + end if method == :delete_all update_counter(-scope.delete_all) 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 9657cb081d..c7d8a84a7e 100644 --- a/activerecord/lib/active_record/associations/has_many_through_association.rb +++ b/activerecord/lib/active_record/associations/has_many_through_association.rb @@ -1,4 +1,3 @@ -require 'active_support/core_ext/object/blank' module ActiveRecord # = Active Record Has Many Through Association @@ -38,6 +37,20 @@ module ActiveRecord super end + def concat_records(records) + ensure_not_nested + + records = super + + if owner.new_record? && records + records.flatten.each do |record| + build_through_record(record) + end + end + + records + end + def insert_record(record, validate = true, raise = false) ensure_not_nested @@ -54,10 +67,6 @@ module ActiveRecord record end - # ActiveRecord::Relation#delete_all needs to support joins before we can use a - # SQL-only implementation. - alias delete_all_on_destroy delete_all - private def through_association @@ -73,7 +82,9 @@ module ActiveRecord # association def build_through_record(record) @through_records[record.object_id] ||= begin - through_record = through_association.build(construct_join_attributes(record)) + ensure_mutable + + through_record = through_association.build through_record.send("#{source_reflection.name}=", record) through_record end @@ -85,10 +96,10 @@ module ActiveRecord @through_records.delete(record.object_id) end - def build_record(attributes, options = {}) + def build_record(attributes) ensure_not_nested - record = super(attributes, options) + record = super(attributes) inverse = source_reflection.inverse_of if inverse @@ -124,7 +135,12 @@ module ActiveRecord def delete_records(records, method) ensure_not_nested - scope = through_association.scoped.where(construct_join_attributes(*records)) + # This is unoptimised; it will load all the target records + # even when we just want to delete everything. + records = load_target if records == :all + + scope = through_association.scope + scope.where! construct_join_attributes(*records) case method when :destroy @@ -168,7 +184,7 @@ module ActiveRecord def find_target return [] unless target_reflection_has_associated_record? - scoped.all + scope.to_a end # NOTE - not sure that we can actually cope with inverses here diff --git a/activerecord/lib/active_record/associations/has_one_association.rb b/activerecord/lib/active_record/associations/has_one_association.rb index 2131edbc20..06bead41de 100644 --- a/activerecord/lib/active_record/associations/has_one_association.rb +++ b/activerecord/lib/active_record/associations/has_one_association.rb @@ -1,26 +1,45 @@ -require 'active_support/core_ext/object/inclusion' module ActiveRecord # = Active Record Belongs To Has One Association module Associations class HasOneAssociation < SingularAssociation #:nodoc: + + def handle_dependency + case options[:dependent] + when :restrict, :restrict_with_exception + raise ActiveRecord::DeleteRestrictionError.new(reflection.name) if load_target + + when :restrict_with_error + if load_target + record = klass.human_attribute_name(reflection.name).downcase + owner.errors.add(:base, :"restrict_dependent_destroy.one", record: record) + false + end + + else + delete + end + end + def replace(record, save = true) raise_on_type_mismatch(record) if record load_target - reflection.klass.transaction do - if target && target != record - remove_target!(options[:dependent]) unless target.destroyed? - end + # If target and record are nil, or target is equal to record, + # we don't need to have transaction. + if (target || record) && target != record + reflection.klass.transaction do + remove_target!(options[:dependent]) if target && !target.destroyed? - if record - set_owner_attributes(record) - set_inverse_instance(record) + if record + set_owner_attributes(record) + set_inverse_instance(record) - if owner.persisted? && save && !record.save - nullify_owner_attributes(record) - set_owner_attributes(target) if target - raise RecordNotSaved, "Failed to save the new associated #{reflection.name}." + if owner.persisted? && save && !record.save + nullify_owner_attributes(record) + set_owner_attributes(target) if target + raise RecordNotSaved, "Failed to save the new associated #{reflection.name}." + end end end end @@ -36,7 +55,7 @@ module ActiveRecord when :destroy target.destroy when :nullify - target.update_attribute(reflection.foreign_key, nil) + target.update_columns(reflection.foreign_key => nil) end end end @@ -52,16 +71,19 @@ module ActiveRecord end def remove_target!(method) - if method.in?([:delete, :destroy]) - target.send(method) - else - nullify_owner_attributes(target) + case method + when :delete + target.delete + when :destroy + target.destroy + else + nullify_owner_attributes(target) - 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 + 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 after its foreign key was set to nil." + 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 index 0d7d28e458..0d3b4dbab1 100644 --- a/activerecord/lib/active_record/associations/join_dependency/join_association.rb +++ b/activerecord/lib/active_record/associations/join_dependency/join_association.rb @@ -92,14 +92,21 @@ module ActiveRecord constraint = build_constraint(reflection, table, key, foreign_table, foreign_key) - conditions = self.conditions[i].dup - conditions << { reflection.type => foreign_klass.base_class.name } if reflection.type + scope_chain_items = scope_chain[i] - conditions.each 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) + if reflection.type + scope_chain_items += [ + ActiveRecord::Relation.new(reflection.klass, table) + .where(reflection.type => foreign_klass.base_class.name) + ] + end + + scope_chain_items.each do |item| + unless item.is_a?(Relation) + item = ActiveRecord::Relation.new(reflection.klass, table).instance_exec(self, &item) + end - constraint = constraint.and(condition) + constraint = constraint.and(item.arel.constraints) unless item.arel.constraints.empty? end relation.from(join(table, constraint)) @@ -137,18 +144,8 @@ module ActiveRecord 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 + def scope_chain + @scope_chain ||= reflection.scope_chain.reverse 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 index 2b1d888a9a..711f7b3ce1 100644 --- a/activerecord/lib/active_record/associations/join_dependency/join_part.rb +++ b/activerecord/lib/active_record/associations/join_dependency/join_part.rb @@ -54,7 +54,7 @@ module ActiveRecord unless @column_names_with_alias @column_names_with_alias = [] - ([primary_key] + (column_names - [primary_key])).each_with_index do |column_name, i| + ([primary_key] + (column_names - [primary_key])).compact.each_with_index do |column_name, i| @column_names_with_alias << [column_name, "#{aliased_prefix}_r#{i}"] end end diff --git a/activerecord/lib/active_record/associations/join_helper.rb b/activerecord/lib/active_record/associations/join_helper.rb index cea6ad6944..5a41b40c8f 100644 --- a/activerecord/lib/active_record/associations/join_helper.rb +++ b/activerecord/lib/active_record/associations/join_helper.rb @@ -19,7 +19,7 @@ module ActiveRecord if reflection.source_macro == :has_and_belongs_to_many tables << alias_tracker.aliased_table_for( - (reflection.source_reflection || reflection).options[:join_table], + (reflection.source_reflection || reflection).join_table, table_alias_for(reflection, true) ) end diff --git a/activerecord/lib/active_record/associations/preloader.rb b/activerecord/lib/active_record/associations/preloader.rb index fafed94ff2..ce5bf15f10 100644 --- a/activerecord/lib/active_record/associations/preloader.rb +++ b/activerecord/lib/active_record/associations/preloader.rb @@ -12,7 +12,7 @@ module ActiveRecord # and all of its books via a single query: # # SELECT * FROM authors - # LEFT OUTER JOIN books ON authors.id = books.id + # LEFT OUTER JOIN books ON authors.id = books.author_id # WHERE authors.name = 'Ken Akamatsu' # # However, this could result in many rows that contain redundant data. After @@ -42,7 +42,7 @@ module ActiveRecord 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 + attr_reader :records, :associations, :preload_scope, :model # Eager loads the named associations for the given Active Record record(s). # @@ -78,15 +78,10 @@ module ActiveRecord # [ :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 + def initialize(records, associations, preload_scope = nil) + @records = Array.wrap(records).compact.uniq + @associations = Array.wrap(associations) + @preload_scope = preload_scope || Relation.new(nil, nil) end def run @@ -110,7 +105,7 @@ module ActiveRecord def preload_hash(association) association.each do |parent, child| - Preloader.new(records, parent, options).run + Preloader.new(records, parent, preload_scope).run Preloader.new(records.map { |record| record.send(parent) }.flatten, child).run end end @@ -125,7 +120,7 @@ module ActiveRecord 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 + preloader_for(reflection).new(klass, records, reflection, preload_scope).run end end end diff --git a/activerecord/lib/active_record/associations/preloader/association.rb b/activerecord/lib/active_record/associations/preloader/association.rb index 253998fb23..cbf5e734ea 100644 --- a/activerecord/lib/active_record/associations/preloader/association.rb +++ b/activerecord/lib/active_record/associations/preloader/association.rb @@ -2,16 +2,16 @@ 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 + attr_reader :owners, :reflection, :preload_scope, :model, :klass + + def initialize(klass, owners, reflection, preload_scope) + @klass = klass + @owners = owners + @reflection = reflection + @preload_scope = preload_scope + @model = owners.first && owners.first.class + @scope = nil + @owners_by_key = nil end def run @@ -24,12 +24,12 @@ module ActiveRecord raise NotImplementedError end - def scoped - @scoped ||= build_scope + def scope + @scope ||= build_scope end def records_for(ids) - scoped.where(association_key.in(ids)) + scope.where(association_key.in(ids)) end def table @@ -77,7 +77,7 @@ module ActiveRecord # 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 + records = sliced.map { |slice| records_for(slice).to_a }.flatten end # Each record may have multiple owners, and vice-versa @@ -92,33 +92,29 @@ module ActiveRecord records_by_owner end + def reflection_scope + @reflection_scope ||= reflection.scope ? klass.unscoped.instance_exec(nil, &reflection.scope) : klass.unscoped + end + def build_scope - scope = klass.scoped + scope = klass.unscoped + scope.default_scoped = true - scope = scope.where(interpolate(options[:conditions])) - scope = scope.where(interpolate(preload_options[:conditions])) + values = reflection_scope.values + preload_values = preload_scope.values - scope = scope.select(preload_options[:select] || options[:select] || table[Arel.star]) - scope = scope.includes(preload_options[:include] || options[:include]) + scope.where_values = Array(values[:where]) + Array(preload_values[:where]) + scope.references_values = Array(values[:references]) + Array(preload_values[:references]) + + scope.select! preload_values[:select] || values[:select] || table[Arel.star] + scope.includes! preload_values[:includes] || values[:includes] if options[:as] - scope = scope.where( - klass.table_name => { - reflection.type => model.base_class.sti_name - } - ) + scope.where!(klass.table_name => { reflection.type => model.base_class.sti_name }) end scope end - - def interpolate(conditions) - if conditions.respond_to?(:to_proc) - klass.send(:instance_eval, &conditions) - else - conditions - 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 index c248aeaaf6..e6cd35e7a1 100644 --- a/activerecord/lib/active_record/associations/preloader/collection_association.rb +++ b/activerecord/lib/active_record/associations/preloader/collection_association.rb @@ -6,7 +6,7 @@ module ActiveRecord private def build_scope - super.order(preload_options[:order] || options[:order]) + super.order(preload_scope.values[:order] || reflection_scope.values[:order]) end def preload 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 index b77b667219..8e8925f0a9 100644 --- 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 @@ -6,7 +6,7 @@ module ActiveRecord def initialize(klass, records, reflection, preload_options) super - @join_table = Arel::Table.new(options[:join_table]).alias('t0') + @join_table = Arel::Table.new(reflection.join_table).alias('t0') end # Unlike the other associations, we want to get a raw array of rows so that we can diff --git a/activerecord/lib/active_record/associations/preloader/has_many_through.rb b/activerecord/lib/active_record/associations/preloader/has_many_through.rb index c6e9ede356..9a662d3f53 100644 --- a/activerecord/lib/active_record/associations/preloader/has_many_through.rb +++ b/activerecord/lib/active_record/associations/preloader/has_many_through.rb @@ -6,7 +6,7 @@ module ActiveRecord def associated_records_by_owner super.each do |owner, records| - records.uniq! if options[:uniq] + records.uniq! if reflection_scope.uniq_value 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 index 848448bb48..24728e9f01 100644 --- a/activerecord/lib/active_record/associations/preloader/has_one.rb +++ b/activerecord/lib/active_record/associations/preloader/has_one.rb @@ -14,7 +14,7 @@ module ActiveRecord private def build_scope - super.order(preload_options[:order] || options[:order]) + super.order(preload_scope.values[:order] || reflection_scope.values[:order]) end end diff --git a/activerecord/lib/active_record/associations/preloader/through_association.rb b/activerecord/lib/active_record/associations/preloader/through_association.rb index ad6374d09a..1c1ba11c44 100644 --- a/activerecord/lib/active_record/associations/preloader/through_association.rb +++ b/activerecord/lib/active_record/associations/preloader/through_association.rb @@ -14,10 +14,7 @@ module ActiveRecord def associated_records_by_owner through_records = through_records_by_owner - ActiveRecord::Associations::Preloader.new( - through_records.values.flatten, - source_reflection.name, options - ).run + Preloader.new(through_records.values.flatten, source_reflection.name, reflection_scope).run through_records.each do |owner, records| records.map! { |r| r.send(source_reflection.name) }.flatten! @@ -28,10 +25,7 @@ module ActiveRecord private def through_records_by_owner - ActiveRecord::Associations::Preloader.new( - owners, through_reflection.name, - through_options - ).run + Preloader.new(owners, through_reflection.name, through_scope).run Hash[owners.map do |owner| through_records = Array.wrap(owner.send(through_reflection.name)) @@ -45,21 +39,22 @@ module ActiveRecord end] end - def through_options - through_options = {} + def through_scope + through_scope = through_reflection.klass.unscoped if options[:source_type] - through_options[:conditions] = { reflection.foreign_type => options[:source_type] } + through_scope.where! reflection.foreign_type => options[:source_type] else - if options[:conditions] - through_options[:include] = options[:include] || options[:source] - through_options[:conditions] = options[:conditions] + unless reflection_scope.where_values.empty? + through_scope.includes_values = reflection_scope.values[:includes] || options[:source] + through_scope.where_values = reflection_scope.values[:where] end - through_options[:order] = options[:order] + through_scope.order! reflection_scope.values[:order] + through_scope.references! reflection_scope.values[:references] end - through_options + through_scope end end end diff --git a/activerecord/lib/active_record/associations/singular_association.rb b/activerecord/lib/active_record/associations/singular_association.rb index a1a921bcb4..32f4557c28 100644 --- a/activerecord/lib/active_record/associations/singular_association.rb +++ b/activerecord/lib/active_record/associations/singular_association.rb @@ -17,16 +17,16 @@ module ActiveRecord replace(record) end - def create(attributes = {}, options = {}, &block) - create_record(attributes, options, &block) + def create(attributes = {}, &block) + create_record(attributes, &block) end - def create!(attributes = {}, options = {}, &block) - create_record(attributes, options, true, &block) + def create!(attributes = {}, &block) + create_record(attributes, true, &block) end - def build(attributes = {}, options = {}) - record = build_record(attributes, options) + def build(attributes = {}) + record = build_record(attributes) yield(record) if block_given? set_new_record(record) record @@ -35,11 +35,11 @@ module ActiveRecord private def create_scope - scoped.scope_for_create.stringify_keys.except(klass.primary_key) + scope.scope_for_create.stringify_keys.except(klass.primary_key) end def find_target - scoped.first.tap { |record| set_inverse_instance(record) } + scope.first.tap { |record| set_inverse_instance(record) } end # Implemented by subclasses @@ -51,8 +51,8 @@ module ActiveRecord replace(record) end - def create_record(attributes, options, raise_error = false) - record = build_record(attributes, options) + def create_record(attributes, raise_error = false) + record = build_record(attributes) yield(record) if block_given? saved = record.save set_new_record(record) diff --git a/activerecord/lib/active_record/associations/through_association.rb b/activerecord/lib/active_record/associations/through_association.rb index f95e5337c2..b9e014735b 100644 --- a/activerecord/lib/active_record/associations/through_association.rb +++ b/activerecord/lib/active_record/associations/through_association.rb @@ -15,7 +15,7 @@ module ActiveRecord scope = super chain[1..-1].each do |reflection| scope = scope.merge( - reflection.klass.scoped.with_default_scope. + reflection.klass.all.with_default_scope. except(:select, :create_with, :includes, :preload, :joins, :eager_load) ) end @@ -37,9 +37,7 @@ module ActiveRecord # 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 + ensure_mutable join_attributes = { source_reflection.foreign_key => @@ -64,7 +62,7 @@ module ActiveRecord # properly support stale-checking for nested associations. def stale_state if through_reflection.macro == :belongs_to - owner[through_reflection.foreign_key].to_s + owner[through_reflection.foreign_key] && owner[through_reflection.foreign_key].to_s end end @@ -73,6 +71,12 @@ module ActiveRecord !owner[through_reflection.foreign_key].nil? end + def ensure_mutable + if source_reflection.macro != :belongs_to + raise HasManyThroughCantAssociateThroughHasOneOrManyReflection.new(owner, reflection) + end + end + def ensure_not_nested if reflection.nested? raise HasManyThroughNestedAssociationsAreReadonly.new(owner, reflection) diff --git a/activerecord/lib/active_record/attribute_assignment.rb b/activerecord/lib/active_record/attribute_assignment.rb index bf9fe70b31..af13b75a9d 100644 --- a/activerecord/lib/active_record/attribute_assignment.rb +++ b/activerecord/lib/active_record/attribute_assignment.rb @@ -1,114 +1,57 @@ -require 'active_support/concern' module ActiveRecord module AttributeAssignment extend ActiveSupport::Concern - include ActiveModel::MassAssignmentSecurity + include ActiveModel::DeprecatedMassAssignmentSecurity + include ActiveModel::ForbiddenAttributesProtection - module ClassMethods - private - - # The primary key and inheritance column can never be set by mass-assignment for security reasons. - def attributes_protected_by_default - default = [ primary_key, inheritance_column ] - default << 'id' unless primary_key.eql? 'id' - default - end - end - - # Allows you to set all the attributes at once by passing in a hash with keys - # matching the attribute names (which again matches the column names). - # - # If any attributes are protected by either +attr_protected+ or - # +attr_accessible+ then only settable attributes will be assigned. - # - # class User < ActiveRecord::Base - # attr_protected :is_admin - # end - # - # user = User.new - # user.attributes = { :username => 'Phusion', :is_admin => true } - # user.username # => "Phusion" - # user.is_admin? # => false - def attributes=(new_attributes) - return unless new_attributes.is_a?(Hash) - - assign_attributes(new_attributes) - end - - # Allows you to set all the attributes for a particular mass-assignment - # security role by passing in a hash of attributes with keys matching - # the attribute names (which again matches the column names) and the role - # name using the :as option. + # Allows you to set all the attributes by passing in a hash of attributes with + # keys matching the attribute names (which again matches the column names). # - # To bypass mass-assignment security you can use the :without_protection => true - # option. - # - # class User < ActiveRecord::Base - # attr_accessible :name - # attr_accessible :name, :is_admin, :as => :admin - # end - # - # user = User.new - # user.assign_attributes({ :name => 'Josh', :is_admin => true }) - # user.name # => "Josh" - # user.is_admin? # => false - # - # user = User.new - # user.assign_attributes({ :name => 'Josh', :is_admin => true }, :as => :admin) - # user.name # => "Josh" - # user.is_admin? # => true - # - # user = User.new - # user.assign_attributes({ :name => 'Josh', :is_admin => true }, :without_protection => true) - # user.name # => "Josh" - # user.is_admin? # => true - def assign_attributes(new_attributes, options = {}) - return unless new_attributes - - attributes = new_attributes.stringify_keys - multi_parameter_attributes = [] + # If the passed hash responds to <tt>permitted?</tt> method and the return value + # of this method is +false+ an <tt>ActiveModel::ForbiddenAttributesError</tt> + # exception is raised. + def assign_attributes(new_attributes) + return if new_attributes.blank? + + attributes = new_attributes.stringify_keys + multi_parameter_attributes = [] nested_parameter_attributes = [] - @mass_assignment_options = options - unless options[:without_protection] - attributes = sanitize_for_mass_assignment(attributes, mass_assignment_role) - end + attributes = sanitize_for_mass_assignment(attributes) attributes.each do |k, v| if k.include?("(") multi_parameter_attributes << [ k, v ] - elsif respond_to?("#{k}=") - if v.is_a?(Hash) - nested_parameter_attributes << [ k, v ] - else - send("#{k}=", v) - end + elsif v.is_a?(Hash) + nested_parameter_attributes << [ k, v ] else - raise(UnknownAttributeError, "unknown attribute: #{k}") + _assign_attribute(k, v) end end - # assign any deferred nested attributes after the base attributes have been set - nested_parameter_attributes.each do |k,v| - send("#{k}=", v) - end - - @mass_assignment_options = nil - assign_multiparameter_attributes(multi_parameter_attributes) + assign_nested_parameter_attributes(nested_parameter_attributes) unless nested_parameter_attributes.empty? + assign_multiparameter_attributes(multi_parameter_attributes) unless multi_parameter_attributes.empty? end - protected + alias attributes= assign_attributes - def mass_assignment_options - @mass_assignment_options ||= {} - end + private - def mass_assignment_role - mass_assignment_options[:as] || :default + def _assign_attribute(k, v) + public_send("#{k}=", v) + rescue NoMethodError + if respond_to?("#{k}=") + raise + else + raise UnknownAttributeError, "unknown attribute: #{k}" + end end - private + # Assign any deferred nested attributes after the base attributes have been set. + def assign_nested_parameter_attributes(pairs) + pairs.each { |k, v| _assign_attribute(k, v) } + 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. @@ -116,91 +59,34 @@ module ActiveRecord # 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 - # attribute will be set to nil. + # attribute will be set to +nil+. def assign_multiparameter_attributes(pairs) execute_callstack_for_multiparameter_attributes( extract_callstack_for_multiparameter_attributes(pairs) ) end - def instantiate_time_object(name, values) - if self.class.send(:create_time_zone_conversion_attribute?, name, column_for_attribute(name)) - Time.zone.local(*values) - else - Time.time_with_datetime_fallback(self.class.default_timezone, *values) - end - end - def execute_callstack_for_multiparameter_attributes(callstack) errors = [] callstack.each do |name, values_with_empty_parameters| begin - send(name + "=", read_value_from_parameter(name, values_with_empty_parameters)) + send("#{name}=", MultiparameterAttribute.new(self, name, values_with_empty_parameters).read_value) rescue => ex - errors << AttributeAssignmentError.new("error on assignment #{values_with_empty_parameters.values.inspect} to #{name}", ex, name) + errors << AttributeAssignmentError.new("error on assignment #{values_with_empty_parameters.values.inspect} to #{name} (#{ex.message})", ex, name) end end unless errors.empty? - raise MultiparameterAssignmentErrors.new(errors), "#{errors.size} error(s) on assignment of multiparameter attributes" - end - end - - def read_value_from_parameter(name, values_hash_from_param) - klass = (self.class.reflect_on_aggregation(name.to_sym) || column_for_attribute(name)).klass - if values_hash_from_param.values.all?{|v|v.nil?} - nil - elsif klass == Time - read_time_parameter_value(name, values_hash_from_param) - elsif klass == Date - read_date_parameter_value(name, values_hash_from_param) - else - read_other_parameter_value(klass, name, values_hash_from_param) - end - end - - def read_time_parameter_value(name, values_hash_from_param) - # If Date bits were not provided, error - raise "Missing Parameter" if [1,2,3].any?{|position| !values_hash_from_param.has_key?(position)} - max_position = extract_max_param_for_multiparameter_attributes(values_hash_from_param, 6) - # If Date bits were provided but blank, then return nil - return nil if (1..3).any? {|position| values_hash_from_param[position].blank?} - - set_values = (1..max_position).collect{|position| values_hash_from_param[position] } - # If Time bits are not there, then default to 0 - (3..5).each {|i| set_values[i] = set_values[i].blank? ? 0 : set_values[i]} - instantiate_time_object(name, set_values) - end - - def read_date_parameter_value(name, values_hash_from_param) - return nil if (1..3).any? {|position| values_hash_from_param[position].blank?} - set_values = [values_hash_from_param[1], values_hash_from_param[2], values_hash_from_param[3]] - begin - Date.new(*set_values) - rescue ArgumentError # if Date.new raises an exception on an invalid date - instantiate_time_object(name, set_values).to_date # we instantiate Time object and convert it back to a date thus using Time's logic in handling invalid dates - end - end - - def read_other_parameter_value(klass, name, values_hash_from_param) - max_position = extract_max_param_for_multiparameter_attributes(values_hash_from_param) - values = (1..max_position).collect do |position| - raise "Missing Parameter" if !values_hash_from_param.has_key?(position) - values_hash_from_param[position] + error_descriptions = errors.map { |ex| ex.message }.join(",") + raise MultiparameterAssignmentErrors.new(errors), "#{errors.size} error(s) on assignment of multiparameter attributes [#{error_descriptions}]" end - klass.new(*values) - end - - def extract_max_param_for_multiparameter_attributes(values_hash_from_param, upper_cap = 100) - [values_hash_from_param.keys.max,upper_cap].min end def extract_callstack_for_multiparameter_attributes(pairs) attributes = { } - pairs.each do |pair| - multiparameter_name, value = pair + pairs.each do |(multiparameter_name, value)| attribute_name = multiparameter_name.split("(").first - attributes[attribute_name] = {} unless attributes.include?(attribute_name) + attributes[attribute_name] ||= {} parameter_value = value.empty? ? nil : type_cast_attribute_value(multiparameter_name, value) attributes[attribute_name][find_parameter_position(multiparameter_name)] ||= parameter_value @@ -217,5 +103,100 @@ module ActiveRecord multiparameter_name.scan(/\(([0-9]*).*\)/).first.first.to_i end + class MultiparameterAttribute #:nodoc: + attr_reader :object, :name, :values, :column + + def initialize(object, name, values) + @object = object + @name = name + @values = values + end + + def read_value + return if values.values.compact.empty? + + @column = object.class.reflect_on_aggregation(name.to_sym) || object.column_for_attribute(name) + klass = column.klass + + if klass == Time + read_time + elsif klass == Date + read_date + else + read_other(klass) + end + end + + private + + def instantiate_time_object(set_values) + if object.class.send(:create_time_zone_conversion_attribute?, name, column) + Time.zone.local(*set_values) + else + Time.time_with_datetime_fallback(object.class.default_timezone, *set_values) + end + end + + def read_time + # If column is a :time (and not :date or :timestamp) there is no need to validate if + # there are year/month/day fields + if column.type == :time + # if the column is a time set the values to their defaults as January 1, 1970, but only if they're nil + { 1 => 1970, 2 => 1, 3 => 1 }.each do |key,value| + values[key] ||= value + end + else + # else column is a timestamp, so if Date bits were not provided, error + validate_missing_parameters!([1,2,3]) + + # If Date bits were provided but blank, then return nil + return if blank_date_parameter? + end + + max_position = extract_max_param(6) + set_values = values.values_at(*(1..max_position)) + # If Time bits are not there, then default to 0 + (3..5).each { |i| set_values[i] = set_values[i].presence || 0 } + instantiate_time_object(set_values) + end + + def read_date + return if blank_date_parameter? + set_values = values.values_at(1,2,3) + begin + Date.new(*set_values) + rescue ArgumentError # if Date.new raises an exception on an invalid date + instantiate_time_object(set_values).to_date # we instantiate Time object and convert it back to a date thus using Time's logic in handling invalid dates + end + end + + def read_other(klass) + max_position = extract_max_param + positions = (1..max_position) + validate_missing_parameters!(positions) + + set_values = values.values_at(*positions) + klass.new(*set_values) + end + + # Checks whether some blank date parameter exists. Note that this is different + # than the validate_missing_parameters! method, since it just checks for blank + # positions instead of missing ones, and does not raise in case one blank position + # exists. The caller is responsible to handle the case of this returning true. + def blank_date_parameter? + (1..3).any? { |position| values[position].blank? } + end + + # If some position is not provided, it errors out a missing parameter exception. + def validate_missing_parameters!(positions) + if missing_parameter = positions.detect { |position| !values.key?(position) } + raise ArgumentError.new("Missing Parameter - #{name}(#{missing_parameter})") + end + end + + def extract_max_param(upper_cap = 100) + [values.keys.max, upper_cap].min + end + end end end diff --git a/activerecord/lib/active_record/attribute_methods.rb b/activerecord/lib/active_record/attribute_methods.rb index 3e27e85f02..ced15bc330 100644 --- a/activerecord/lib/active_record/attribute_methods.rb +++ b/activerecord/lib/active_record/attribute_methods.rb @@ -1,5 +1,4 @@ require 'active_support/core_ext/enumerable' -require 'active_support/deprecation' module ActiveRecord # = Active Record Attribute Methods @@ -16,19 +15,6 @@ module ActiveRecord include TimeZoneConversion include Dirty include Serialization - - # 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) - end end module ClassMethods @@ -49,14 +35,6 @@ module ActiveRecord @attribute_methods_generated ||= false end - # We will define the methods as instance methods, but will call them as singleton - # methods. This allows us to use method_defined? to check if the method exists, - # which is fast and won't give any false positives from the ancestors (because - # there are no ancestors). - def generated_external_attribute_methods - @generated_external_attribute_methods ||= Module.new { extend self } - end - def undefine_attribute_methods super if attribute_methods_generated? @attribute_methods_generated = false @@ -157,7 +135,9 @@ module ActiveRecord # Returns a hash of all the attributes with their names as keys and the values of the attributes as values. def attributes - Hash[@attributes.map { |name, _| [name, read_attribute(name)] }] + attribute_names.each_with_object({}) { |name, attrs| + attrs[name] = read_attribute(name) + } end # Returns an <tt>#inspect</tt>-like string for the value of the @@ -189,7 +169,7 @@ module ActiveRecord # 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.nil? || (value.respond_to?(:empty?) && !value.empty?) + !value.nil? && !(value.respond_to?(:empty?) && value.empty?) end # Returns the column object for the named attribute. @@ -198,6 +178,19 @@ module ActiveRecord self.class.columns_hash[name.to_s] 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) + end + protected def clone_attributes(reader_method = :read_attribute, attributes = {}) @@ -214,37 +207,64 @@ module ActiveRecord value end - # 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 = {} - klass = self.class - arel_table = klass.arel_table + def arel_attributes_with_values_for_create(pk_attribute_allowed) + arel_attributes_with_values(attributes_for_create(pk_attribute_allowed)) + end - attribute_names.each do |name| - if (column = column_for_attribute(name)) && (include_primary_key || !column.primary) + def arel_attributes_with_values_for_update(attribute_names) + arel_attributes_with_values(attributes_for_update(attribute_names)) + end - if include_readonly_attributes || !self.class.readonly_attributes.include?(name) + def attribute_method?(attr_name) + defined?(@attributes) && @attributes.include?(attr_name) + end - value = if klass.serialized_attributes.include?(name) - @attributes[name].serialized_value - 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 + private - attrs[arel_table[name]] = value - end - end - end + # Returns a Hash of the Arel::Attributes and attribute values that have been + # type casted for use in an Arel insert/update method. + def arel_attributes_with_values(attribute_names) + attrs = {} + arel_table = self.class.arel_table + attribute_names.each do |name| + attrs[arel_table[name]] = typecasted_attribute_value(name) + end attrs end - def attribute_method?(attr_name) - defined?(@attributes) && @attributes.include?(attr_name) + # Filters the primary keys and readonly attributes from the attribute names. + def attributes_for_update(attribute_names) + attribute_names.select do |name| + column_for_attribute(name) && !pk_attribute?(name) && !readonly_attribute?(name) + end + end + + # Filters out the primary keys, from the attribute names, when the primary + # key is to be generated (e.g. the id attribute has no value). + def attributes_for_create(pk_attribute_allowed) + @attributes.keys.select do |name| + column_for_attribute(name) && (pk_attribute_allowed || !pk_attribute?(name)) + end + end + + def readonly_attribute?(name) + self.class.readonly_attributes.include?(name) + end + + def pk_attribute?(name) + column_for_attribute(name).primary + end + + def typecasted_attribute_value(name) + if self.class.serialized_attributes.include?(name) + @attributes[name].serialized_value + else + # FIXME: we need @attributes to be used consistently. + # If the values stored in @attributes were already typecasted, this code + # could be simplified + read_attribute(name) + 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 433d508357..60e5b0e2bb 100644 --- a/activerecord/lib/active_record/attribute_methods/dirty.rb +++ b/activerecord/lib/active_record/attribute_methods/dirty.rb @@ -1,12 +1,16 @@ -require 'active_support/core_ext/class/attribute' -require 'active_support/core_ext/object/blank' +require 'active_support/core_ext/module/attribute_accessors' module ActiveRecord + ActiveSupport.on_load(:active_record_config) do + mattr_accessor :partial_updates, instance_accessor: false + self.partial_updates = true + end + module AttributeMethods module Dirty extend ActiveSupport::Concern + include ActiveModel::Dirty - include AttributeMethods::Write included do if self < ::ActiveRecord::Timestamp @@ -14,7 +18,6 @@ module ActiveRecord end config_attribute :partial_updates - self.partial_updates = true end # Attempts to +save+ the record and clears changed attributes if successful. @@ -22,8 +25,6 @@ module ActiveRecord if status = super @previously_changed = changes @changed_attributes.clear - elsif IdentityMap.enabled? - IdentityMap.remove(self) end status end @@ -34,9 +35,6 @@ 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. @@ -58,8 +56,6 @@ module ActiveRecord @changed_attributes.delete(attr) unless _field_changed?(attr, old, value) else old = clone_attribute_value(:read_attribute, attr) - # Save Time objects as TimeWithZone if time_zone_aware_attributes == true - old = old.in_time_zone if clone_with_time_zone_conversion_attribute?(attr, old) @changed_attributes[attr] = old if _field_changed?(attr, old, value) end @@ -79,11 +75,8 @@ module ActiveRecord def _field_changed?(attr, old, value) if column = column_for_attribute(attr) - if column.number? && column.null && (old.nil? || old == 0) && value.blank? - # For nullable numeric columns, NULL gets stored in database for blank (i.e. '') values. - # Hence we don't record it as a change if the value changes from nil to ''. - # If an old value of 0 is set to '' we want this to get changed to nil as otherwise it'll - # be typecast back to 0 (''.to_i => 0) + if column.number? && (changes_from_nil_to_empty_string?(column, old, value) || + changes_from_zero_to_string?(old, value)) value = nil else value = column.type_cast(value) @@ -93,8 +86,17 @@ module ActiveRecord old != value end - def clone_with_time_zone_conversion_attribute?(attr, old) - old.class.name == "Time" && time_zone_aware_attributes && !self.skip_time_zone_conversion_for_attributes.include?(attr.to_sym) + def changes_from_nil_to_empty_string?(column, old, value) + # For nullable numeric columns, NULL gets stored in database for blank (i.e. '') values. + # Hence we don't record it as a change if the value changes from nil to ''. + # If an old value of 0 is set to '' we want this to get changed to nil as otherwise it'll + # be typecast back to 0 (''.to_i => 0) + column.null && (old.nil? || old == 0) && value.blank? + end + + def changes_from_zero_to_string?(old, value) + # For columns with old 0 and value non-empty string + old == 0 && value.is_a?(String) && value.present? && value != '0' 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 8e0b3e1402..aa6704d5c9 100644 --- a/activerecord/lib/active_record/attribute_methods/primary_key.rb +++ b/activerecord/lib/active_record/attribute_methods/primary_key.rb @@ -18,7 +18,7 @@ module ActiveRecord # Sets the primary key value def id=(value) - write_attribute(self.class.primary_key, value) + write_attribute(self.class.primary_key, value) if self.class.primary_key end # Queries the primary key value @@ -43,12 +43,6 @@ module ActiveRecord if attr_name == primary_key && attr_name != 'id' generated_attribute_methods.send(:alias_method, :id, primary_key) - generated_external_attribute_methods.module_eval <<-CODE, __FILE__, __LINE__ - def id(v, attributes, attributes_cache, attr_name) - attr_name = '#{primary_key}' - send(attr_name, attributes[attr_name], attributes, attributes_cache, attr_name) - end - CODE end end @@ -59,8 +53,7 @@ module ActiveRecord end # Defines the primary key field -- can be overridden in subclasses. Overwriting will negate any effect of the - # primary_key_prefix_type setting, though. Since primary keys are usually protected from mass assignment, - # remember to let your database generate them or include the key in +attr_accessible+. + # primary_key_prefix_type setting, though. def primary_key @primary_key = reset_primary_key unless defined? @primary_key @primary_key @@ -111,9 +104,8 @@ module ActiveRecord # end # Project.primary_key # => "foo_id" def primary_key=(value) - @original_primary_key = @primary_key if defined?(@primary_key) - @primary_key = value && value.to_s - @quoted_primary_key = nil + @primary_key = value && value.to_s + @quoted_primary_key = nil end end end diff --git a/activerecord/lib/active_record/attribute_methods/query.rb b/activerecord/lib/active_record/attribute_methods/query.rb index 1e841dc8e0..0f9723febb 100644 --- a/activerecord/lib/active_record/attribute_methods/query.rb +++ b/activerecord/lib/active_record/attribute_methods/query.rb @@ -1,5 +1,3 @@ -require 'active_support/core_ext/object/blank' - module ActiveRecord module AttributeMethods module Query @@ -10,7 +8,7 @@ module ActiveRecord end def query_attribute(attr_name) - value = read_attribute(attr_name) + value = read_attribute(attr_name) { |n| missing_attribute(n, caller) } case value when true then true diff --git a/activerecord/lib/active_record/attribute_methods/read.rb b/activerecord/lib/active_record/attribute_methods/read.rb index 846ac03d82..1a4cb25dd7 100644 --- a/activerecord/lib/active_record/attribute_methods/read.rb +++ b/activerecord/lib/active_record/attribute_methods/read.rb @@ -1,4 +1,8 @@ module ActiveRecord + ActiveSupport.on_load(:active_record_config) do + mattr_accessor :attribute_types_cached_by_default, instance_accessor: false + end + module AttributeMethods module Read extend ActiveSupport::Concern @@ -6,8 +10,7 @@ module ActiveRecord ATTRIBUTE_TYPES_CACHED_BY_DEFAULT = [:datetime, :timestamp, :time, :date] included do - config_attribute :attribute_types_cached_by_default, :global => true - self.attribute_types_cached_by_default = ATTRIBUTE_TYPES_CACHED_BY_DEFAULT + config_attribute :attribute_types_cached_by_default end module ClassMethods @@ -29,35 +32,8 @@ module ActiveRecord cached_attributes.include?(attr_name) end - def undefine_attribute_methods - generated_external_attribute_methods.module_eval do - instance_methods.each { |m| undef_method(m) } - end - - super - end - - def type_cast_attribute(attr_name, attributes, cache = {}) #:nodoc: - return unless attr_name - attr_name = attr_name.to_s - - if generated_external_attribute_methods.method_defined?(attr_name) - if attributes.has_key?(attr_name) || attr_name == 'id' - generated_external_attribute_methods.send(attr_name, attributes[attr_name], attributes, cache, attr_name) - end - elsif !attribute_methods_generated? - # If we haven't generated the caster methods yet, do that and - # then try again - define_attribute_methods - type_cast_attribute(attr_name, attributes, cache) - else - # If we get here, the attribute has no associated DB column, so - # just return it verbatim. - attributes[attr_name] - end - end - protected + # We want to generate the methods via module_eval rather than define_method, # because define_method is slower on dispatch and uses more memory (because it # creates a closure). @@ -67,19 +43,9 @@ module ActiveRecord # we first define with the __temp__ identifier, and then use alias method to # rename it to what we want. def define_method_attribute(attr_name) - cast_code = attribute_cast_code(attr_name) - generated_attribute_methods.module_eval <<-STR, __FILE__, __LINE__ + 1 def __temp__ - #{internal_attribute_access_code(attr_name, cast_code)} - end - alias_method '#{attr_name}', :__temp__ - undef_method :__temp__ - STR - - generated_external_attribute_methods.module_eval <<-STR, __FILE__, __LINE__ + 1 - def __temp__(v, attributes, attributes_cache, attr_name) - #{external_attribute_access_code(attr_name, cast_code)} + read_attribute(:'#{attr_name}') { |n| missing_attribute(n, caller) } end alias_method '#{attr_name}', :__temp__ undef_method :__temp__ @@ -87,6 +53,7 @@ module ActiveRecord end private + def cacheable_column?(column) if attribute_types_cached_by_default == ATTRIBUTE_TYPES_CACHED_BY_DEFAULT ! serialized_attributes.include? column.name @@ -94,33 +61,27 @@ module ActiveRecord attribute_types_cached_by_default.include?(column.type) end end - - def internal_attribute_access_code(attr_name, cast_code) - "read_attribute('#{attr_name}') { |n| missing_attribute(n, caller) }" - end - - def external_attribute_access_code(attr_name, cast_code) - access_code = "v && #{cast_code}" - - if cache_attribute?(attr_name) - access_code = "attributes_cache[attr_name] ||= (#{access_code})" - end - - access_code - end - - def attribute_cast_code(attr_name) - columns_hash[attr_name].type_cast_code('v') - end end + ActiveRecord::Model.attribute_types_cached_by_default = ATTRIBUTE_TYPES_CACHED_BY_DEFAULT + # 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) + return unless attr_name + name_sym = attr_name.to_sym + # If it's cached, just return it - @attributes_cache.fetch(attr_name) { |name| + # We use #[] first as a perf optimization for non-nil values. See https://gist.github.com/3552829. + @attributes_cache[name_sym] || @attributes_cache.fetch(name_sym) { + name = attr_name.to_s + column = @columns_hash.fetch(name) { - return self.class.type_cast_attribute(name, @attributes, @attributes_cache) + return @attributes.fetch(name) { + if name_sym == :id && self.class.primary_key != name + read_attribute(self.class.primary_key) + end + } } value = @attributes.fetch(name) { @@ -128,7 +89,7 @@ module ActiveRecord } if self.class.cache_attribute?(name) - @attributes_cache[name] = column.type_cast(value) + @attributes_cache[name_sym] = column.type_cast(value) else column.type_cast value end diff --git a/activerecord/lib/active_record/attribute_methods/serialization.rb b/activerecord/lib/active_record/attribute_methods/serialization.rb index 165785c8fb..bdda5bc009 100644 --- a/activerecord/lib/active_record/attribute_methods/serialization.rb +++ b/activerecord/lib/active_record/attribute_methods/serialization.rb @@ -6,10 +6,46 @@ module ActiveRecord included do # Returns a hash of all the attributes that have been specified for serialization as # keys and their class restriction as values. - config_attribute :serialized_attributes + class_attribute :serialized_attributes, instance_accessor: false self.serialized_attributes = {} end + module ClassMethods + # If you have an attribute that needs to be saved to the database as an object, and retrieved as the same object, + # then specify the name of that attribute using this method and it will be handled automatically. + # The serialization is done through YAML. If +class_name+ is specified, the serialized object must be of that + # class on retrieval or SerializationTypeMismatch will be raised. + # + # ==== Parameters + # + # * +attr_name+ - The field name that should be serialized. + # * +class_name+ - Optional, class name that the object type should be equal to. + # + # ==== Example + # # Serialize a preferences attribute + # class User < ActiveRecord::Base + # serialize :preferences + # end + def serialize(attr_name, class_name = Object) + include Behavior + + coder = if [:load, :dump].all? { |x| class_name.respond_to?(x) } + class_name + else + Coders::YAMLColumn.new(class_name) + end + + # merge new serialized attribute and create new hash to ensure that each class in inheritance hierarchy + # has its own hash of own serialized attributes + self.serialized_attributes = serialized_attributes.merge(attr_name.to_s => coder) + end + end + + def serialized_attributes + ActiveSupport::Deprecation.warn("Instance level serialized_attributes method is deprecated, please use class level method.") + defined?(@serialized_attributes) ? @serialized_attributes : self.class.serialized_attributes + end + class Type # :nodoc: def initialize(column) @column = column @@ -44,70 +80,50 @@ module ActiveRecord end end - module ClassMethods - # If you have an attribute that needs to be saved to the database as an object, and retrieved as the same object, - # then specify the name of that attribute using this method and it will be handled automatically. - # The serialization is done through YAML. If +class_name+ is specified, the serialized object must be of that - # class on retrieval or SerializationTypeMismatch will be raised. - # - # ==== Parameters - # - # * +attr_name+ - The field name that should be serialized. - # * +class_name+ - Optional, class name that the object type should be equal to. - # - # ==== Example - # # Serialize a preferences attribute - # class User < ActiveRecord::Base - # serialize :preferences - # end - def serialize(attr_name, class_name = Object) - coder = if [:load, :dump].all? { |x| class_name.respond_to?(x) } - class_name - else - Coders::YAMLColumn.new(class_name) - end + # This is only added to the model when serialize is called, which + # ensures we do not make things slower when serialization is not used. + module Behavior #:nodoc: + extend ActiveSupport::Concern - # 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 - - def initialize_attributes(attributes) #:nodoc: - super + module ClassMethods + def initialize_attributes(attributes, options = {}) + serialized = (options.delete(:serialized) { true }) ? :serialized : :unserialized + super(attributes, options) - serialized_attributes.each do |key, coder| - if attributes.key?(key) - attributes[key] = Attribute.new(coder, attributes[key], :serialized) + serialized_attributes.each do |key, coder| + if attributes.key?(key) + attributes[key] = Attribute.new(coder, attributes[key], serialized) + end end + + attributes end - attributes - end + private - private + def attribute_cast_code(attr_name) + if serialized_attributes.include?(attr_name) + "v.unserialized_value" + else + super + end + end + end - def attribute_cast_code(attr_name) - if serialized_attributes.include?(attr_name) - "v.unserialized_value" + def type_cast_attribute_for_write(column, value) + if column && coder = self.class.serialized_attributes[column.name] + Attribute.new(coder, value, :unserialized) else super end end - end - - def type_cast_attribute_for_write(column, value) - if column && coder = self.class.serialized_attributes[column.name] - Attribute.new(coder, value, :unserialized) - else - super - end - end - def read_attribute_before_type_cast(attr_name) - if serialized_attributes.include?(attr_name) - super.unserialized_value - else - super + def read_attribute_before_type_cast(attr_name) + if self.class.serialized_attributes.include?(attr_name) + super.unserialized_value + else + super + end end end end 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 20372c5c18..9647d03be4 100644 --- a/activerecord/lib/active_record/attribute_methods/time_zone_conversion.rb +++ b/activerecord/lib/active_record/attribute_methods/time_zone_conversion.rb @@ -1,7 +1,13 @@ -require 'active_support/core_ext/class/attribute' -require 'active_support/core_ext/object/inclusion' module ActiveRecord + ActiveSupport.on_load(:active_record_config) do + mattr_accessor :time_zone_aware_attributes, instance_accessor: false + self.time_zone_aware_attributes = false + + mattr_accessor :skip_time_zone_conversion_for_attributes, instance_accessor: false + self.skip_time_zone_conversion_for_attributes = [] + end + module AttributeMethods module TimeZoneConversion class Type # :nodoc: @@ -22,11 +28,8 @@ module ActiveRecord extend ActiveSupport::Concern included do - config_attribute :time_zone_aware_attributes, :global => true - self.time_zone_aware_attributes = false - + config_attribute :time_zone_aware_attributes, global: true config_attribute :skip_time_zone_conversion_for_attributes - self.skip_time_zone_conversion_for_attributes = [] end module ClassMethods @@ -56,9 +59,14 @@ module ActiveRecord 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}, original_time) - @attributes_cache["#{attr_name}"] = time + zoned_time = time && time.in_time_zone rescue nil + rounded_time = round_usec(zoned_time) + rounded_value = round_usec(read_attribute("#{attr_name}")) + if (rounded_value != rounded_time) || (!rounded_value && original_time) + write_attribute("#{attr_name}", original_time) + #{attr_name}_will_change! + @attributes_cache[:"#{attr_name}"] = zoned_time + end end EOV generated_attribute_methods.module_eval(method_body, __FILE__, line) @@ -74,6 +82,12 @@ module ActiveRecord [:datetime, :timestamp].include?(column.type) end end + + private + def round_usec(value) + return unless value + value.change(:usec => 0) + end end end end diff --git a/activerecord/lib/active_record/attribute_methods/write.rb b/activerecord/lib/active_record/attribute_methods/write.rb index 50435921b1..5a39cb0125 100644 --- a/activerecord/lib/active_record/attribute_methods/write.rb +++ b/activerecord/lib/active_record/attribute_methods/write.rb @@ -25,13 +25,13 @@ module ActiveRecord def write_attribute(attr_name, value) attr_name = attr_name.to_s attr_name = self.class.primary_key if attr_name == 'id' && self.class.primary_key - @attributes_cache.delete(attr_name) + @attributes_cache.delete(attr_name.to_sym) column = column_for_attribute(attr_name) # If we're dealing with a binary column, write the data to the cache # so we don't attempt to typecast multiple times. if column && column.binary? - @attributes_cache[attr_name] = value + @attributes_cache[attr_name.to_sym] = value end if column || @attributes.has_key?(attr_name) diff --git a/activerecord/lib/active_record/autosave_association.rb b/activerecord/lib/active_record/autosave_association.rb index 0468a73c7d..a30f888a7a 100644 --- a/activerecord/lib/active_record/autosave_association.rb +++ b/activerecord/lib/active_record/autosave_association.rb @@ -28,7 +28,7 @@ module ActiveRecord # Association with autosave option defines several callbacks on your # model (before_save, after_create, after_update). Please note that # callbacks are executed in the order they were defined in - # model. You should avoid modyfing the association content, before + # model. You should avoid modifying the association content, before # autosave callbacks are executed. Placing your callbacks after # associations is usually a good practice. # @@ -78,7 +78,7 @@ module ActiveRecord # When <tt>:autosave</tt> is not declared new children are saved when their parent is saved: # # class Post - # has_many :comments # :autosave option is no declared + # has_many :comments # :autosave option is not declared # end # # post = Post.new(:title => 'ruby rocks') @@ -93,7 +93,8 @@ module ActiveRecord # post.comments.create(:body => 'hello world') # post.save # => saves both post and comment # - # When <tt>:autosave</tt> is true all children is saved, no matter whether they are new records: + # When <tt>:autosave</tt> is true all children are saved, no matter whether they + # are new records or not: # # class Post # has_many :comments, :autosave => true @@ -126,23 +127,17 @@ module ActiveRecord module AutosaveAssociation extend ActiveSupport::Concern - 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 + super end end included do - ASSOCIATION_TYPES.each do |type| - Associations::Builder.const_get(type).send(:include, AssociationBuilderExtension) + Associations::Builder::Association.class_eval do + self.valid_options << :autosave + include AssociationBuilderExtension end end @@ -190,23 +185,21 @@ module ActiveRecord # 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 + elsif reflection.macro == :has_one + define_method(save_method) { save_has_one_association(reflection) } + # 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 - if reflection.macro == :has_one - define_method(save_method) { save_has_one_association(reflection) } - # 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_non_cyclic_method(save_method, reflection) { save_belongs_to_association(reflection) } - before_save save_method - end + define_non_cyclic_method(save_method, reflection) { save_belongs_to_association(reflection) } + before_save save_method end end @@ -329,14 +322,14 @@ module ActiveRecord autosave = reflection.options[:autosave] if records = associated_records_to_validate_or_save(association, @new_record_before_save, autosave) - begin + records_to_destroy = [] records.each do |record| next if record.destroyed? saved = true if autosave && record.marked_for_destruction? - association.proxy.destroy(record) + records_to_destroy << record elsif autosave != false && (@new_record_before_save || record.new_record?) if autosave saved = association.insert_record(record, false) @@ -349,11 +342,10 @@ module ActiveRecord raise ActiveRecord::Rollback unless saved end - rescue - records.each {|x| IdentityMap.remove(x) } if IdentityMap.enabled? - raise - end + records_to_destroy.each do |record| + association.destroy(record) + end end # reconstruct the scope now that we know the owner's id diff --git a/activerecord/lib/active_record/base.rb b/activerecord/lib/active_record/base.rb index d4d0220fb7..a4705b24ca 100644 --- a/activerecord/lib/active_record/base.rb +++ b/activerecord/lib/active_record/base.rb @@ -4,7 +4,6 @@ require 'active_support/benchmarkable' require 'active_support/dependencies' require 'active_support/descendants_tracker' 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/array/extract_options' @@ -13,11 +12,8 @@ require 'active_support/core_ext/hash/indifferent_access' 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/introspection' require 'active_support/core_ext/object/duplicable' -require 'active_support/core_ext/object/blank' -require 'active_support/deprecation' require 'arel' require 'active_record/errors' require 'active_record/log_subscriber' @@ -201,6 +197,9 @@ module ActiveRecord #:nodoc: # # Now 'Bob' exist and is an 'admin' # User.find_or_create_by_name('Bob', :age => 40) { |u| u.admin = true } # + # Adding an exclamation point (!) on to the end of <tt>find_or_create_by_</tt> will + # raise an <tt>ActiveRecord::RecordInvalid</tt> error if the new record is invalid. + # # 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. # @@ -301,21 +300,22 @@ module ActiveRecord #:nodoc: # (or a bad spelling of an existing one). # * 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> + # * 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 + # triggered the error. + # * 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 # AttributeAssignmentError # objects that should be inspected to determine which attributes triggered the errors. - # * 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 - # triggered the error. + # * RecordInvalid - raised by save! and create! when the record is invalid. + # * 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. + # * SerializationTypeMismatch - The serialized object wasn't of the class specified as the second parameter. + # * StatementInvalid - The database server rejected the SQL statement. The precise error is added in the message. # # *Note*: The attributes listed are class-level attributes (accessible from both the class and instance level). # So it's possible to assign a logger to the class through <tt>Base.logger=</tt> which will then be used by all @@ -325,4 +325,4 @@ module ActiveRecord #:nodoc: end end -ActiveSupport.run_load_hooks(:active_record, ActiveRecord::Model::DeprecationProxy) +ActiveSupport.run_load_hooks(:active_record, ActiveRecord::Model::DeprecationProxy.new) diff --git a/activerecord/lib/active_record/callbacks.rb b/activerecord/lib/active_record/callbacks.rb index a050fabf35..111208d0b9 100644 --- a/activerecord/lib/active_record/callbacks.rb +++ b/activerecord/lib/active_record/callbacks.rb @@ -34,7 +34,7 @@ module ActiveRecord # Examples: # class CreditCard < ActiveRecord::Base # # Strip everything but digits, so the user can specify "555 234 34" or - # # "5552-3434" or both will mean "55523434" + # # "5552-3434" and both will mean "55523434" # before_validation(:on => :create) do # self.number = number.gsub(/[^0-9]/, "") if attribute_present?("number") # end @@ -231,30 +231,6 @@ module ActiveRecord # Returns true or false depending on whether the proc is contained in the before_save callback chain on a Topic model. # module Callbacks - # We can't define callbacks directly on ActiveRecord::Model because - # it is a module. So we queue up the definitions and execute them - # when ActiveRecord::Model is included. - module Register #:nodoc: - def self.extended(base) - base.config_attribute :_callbacks_register - base._callbacks_register = [] - end - - def self.setup(base) - base._callbacks_register.each do |item| - base.send(*item) - end - end - - def define_callbacks(*args) - self._callbacks_register << [:define_callbacks, *args] - end - - def define_model_callbacks(*args) - self._callbacks_register << [:define_model_callbacks, *args] - end - end - extend ActiveSupport::Concern CALLBACKS = [ diff --git a/activerecord/lib/active_record/coders/yaml_column.rb b/activerecord/lib/active_record/coders/yaml_column.rb index 77af540c3e..f17e7158de 100644 --- a/activerecord/lib/active_record/coders/yaml_column.rb +++ b/activerecord/lib/active_record/coders/yaml_column.rb @@ -1,12 +1,10 @@ +require 'yaml' + module ActiveRecord # :stopdoc: module Coders class YAMLColumn - RESCUE_ERRORS = [ ArgumentError ] - - if defined?(Psych) && defined?(Psych::SyntaxError) - RESCUE_ERRORS << Psych::SyntaxError - end + RESCUE_ERRORS = [ ArgumentError, Psych::SyntaxError ] attr_accessor :object_class @@ -15,7 +13,13 @@ module ActiveRecord end def dump(obj) - YAML.dump(obj) unless obj.nil? + return if obj.nil? + + unless obj.is_a?(object_class) + raise SerializationTypeMismatch, + "Attribute was supposed to be a #{object_class}, but was a #{obj.class}. -- #{obj.inspect}" + end + YAML.dump obj end def load(yaml) 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 06b9bc5765..42bd16db80 100644 --- a/activerecord/lib/active_record/connection_adapters/abstract/connection_pool.rb +++ b/activerecord/lib/active_record/connection_adapters/abstract/connection_pool.rb @@ -5,17 +5,11 @@ require 'active_support/core_ext/module/deprecation' module ActiveRecord # Raised when a connection could not be obtained within the connection - # acquisition timeout period. + # acquisition timeout period: because max connections in pool + # are in use. class ConnectionTimeoutError < ConnectionNotEstablished end - # Raised when a connection pool is full and another connection is requested - class PoolFullError < ConnectionNotEstablished - def initialize size, timeout - super("Connection pool of size #{size} and timeout #{timeout}s is full") - end - end - module ConnectionAdapters # Connection pool base class for managing Active Record database # connections. @@ -57,13 +51,150 @@ module ActiveRecord # # == Options # - # There are two connection-pooling-related options that you can add to + # There are several connection-pooling-related options that you can add to # your database connection configuration: # # * +pool+: number indicating size of connection pool (default 5) - # * +wait_timeout+: number of seconds to block and wait for a connection + # * +checkout_timeout+: number of seconds to block and wait for a connection # before giving up and raising a timeout error (default 5 seconds). + # * +reaping_frequency+: frequency in seconds to periodically run the + # Reaper, which attempts to find and close dead connections, which can + # occur if a programmer forgets to close a connection at the end of a + # thread or a thread dies unexpectedly. (Default nil, which means don't + # run the Reaper). + # * +dead_connection_timeout+: number of seconds from last checkout + # after which the Reaper will consider a connection reapable. (default + # 5 seconds). class ConnectionPool + # Threadsafe, fair, FIFO queue. Meant to be used by ConnectionPool + # with which it shares a Monitor. But could be a generic Queue. + # + # The Queue in stdlib's 'thread' could replace this class except + # stdlib's doesn't support waiting with a timeout. + class Queue + def initialize(lock = Monitor.new) + @lock = lock + @cond = @lock.new_cond + @num_waiting = 0 + @queue = [] + end + + # Test if any threads are currently waiting on the queue. + def any_waiting? + synchronize do + @num_waiting > 0 + end + end + + # Return the number of threads currently waiting on this + # queue. + def num_waiting + synchronize do + @num_waiting + end + end + + # Add +element+ to the queue. Never blocks. + def add(element) + synchronize do + @queue.push element + @cond.signal + end + end + + # If +element+ is in the queue, remove and return it, or nil. + def delete(element) + synchronize do + @queue.delete(element) + end + end + + # Remove all elements from the queue. + def clear + synchronize do + @queue.clear + end + end + + # Remove the head of the queue. + # + # If +timeout+ is not given, remove and return the head the + # queue if the number of available elements is strictly + # greater than the number of threads currently waiting (that + # is, don't jump ahead in line). Otherwise, return nil. + # + # If +timeout+ is given, block if it there is no element + # available, waiting up to +timeout+ seconds for an element to + # become available. + # + # Raises: + # - ConnectionTimeoutError if +timeout+ is given and no element + # becomes available after +timeout+ seconds, + def poll(timeout = nil) + synchronize do + if timeout + no_wait_poll || wait_poll(timeout) + else + no_wait_poll + end + end + end + + private + + def synchronize(&block) + @lock.synchronize(&block) + end + + # Test if the queue currently contains any elements. + def any? + !@queue.empty? + end + + # A thread can remove an element from the queue without + # waiting if an only if the number of currently available + # connections is strictly greater than the number of waiting + # threads. + def can_remove_no_wait? + @queue.size > @num_waiting + end + + # Removes and returns the head of the queue if possible, or nil. + def remove + @queue.shift + end + + # Remove and return the head the queue if the number of + # available elements is strictly greater than the number of + # threads currently waiting. Otherwise, return nil. + def no_wait_poll + remove if can_remove_no_wait? + end + + # Waits on the queue up to +timeout+ seconds, then removes and + # returns the head of the queue. + def wait_poll(timeout) + @num_waiting += 1 + + t0 = Time.now + elapsed = 0 + loop do + @cond.wait(timeout - elapsed) + + return remove if any? + + elapsed = Time.now - t0 + if elapsed >= timeout + msg = 'could not obtain a database connection within %0.3f seconds (waited %0.3f seconds)' % + [timeout, elapsed] + raise ConnectionTimeoutError, msg + end + end + ensure + @num_waiting -= 1 + end + end + # Every +frequency+ seconds, the reaper will call +reap+ on +pool+. # A reaper instantiated with a nil frequency will never reap the # connection pool. @@ -91,7 +222,7 @@ module ActiveRecord include MonitorMixin - attr_accessor :automatic_reconnect, :timeout + attr_accessor :automatic_reconnect, :checkout_timeout, :dead_connection_timeout attr_reader :spec, :connections, :size, :reaper # Creates a new ConnectionPool object. +spec+ is a ConnectionSpecification @@ -108,7 +239,8 @@ module ActiveRecord # The cache of reserved connections mapped to threads @reserved_connections = {} - @timeout = spec.config[:wait_timeout] || 5 + @checkout_timeout = spec.config[:checkout_timeout] || 5 + @dead_connection_timeout = spec.config[:dead_connection_timeout] @reaper = Reaper.new self, spec.config[:reaping_frequency] @reaper.run @@ -117,6 +249,16 @@ module ActiveRecord @connections = [] @automatic_reconnect = true + + @available = Queue.new self + end + + # Hack for tests to be able to add connections. Do not call outside of tests + def insert_connection_for_test!(c) #:nodoc: + synchronize do + @connections << c + @available.add c + end end # Retrieve the connection associated with the current thread, or call @@ -125,21 +267,28 @@ module ActiveRecord # #connection can be called any number of times; the connection is # held in a hash keyed by the thread id. def connection - @reserved_connections[current_connection_id] ||= checkout + synchronize do + @reserved_connections[current_connection_id] ||= checkout + end end - # Check to see if there is an active connection in this connection - # pool. + # Is there an open connection that is being used for the current thread? def active_connection? - active_connections.any? + synchronize do + @reserved_connections.fetch(current_connection_id) { + return false + }.in_use? + end end # Signal that the thread is finished with the current connection. # #release_connection releases the connection-thread association # and returns the connection to the pool. def release_connection(with_id = current_connection_id) - conn = @reserved_connections.delete(with_id) - checkin conn if conn + synchronize do + conn = @reserved_connections.delete(with_id) + checkin conn if conn + end end # If a connection already exists yield it to the block. If no connection @@ -167,6 +316,7 @@ module ActiveRecord conn.disconnect! end @connections = [] + @available.clear end end @@ -181,22 +331,17 @@ module ActiveRecord @connections.delete_if do |conn| conn.requires_reloading? end - end - end - - # Verify active connections and remove and disconnect connections - # associated with stale threads. - def verify_active_connections! #:nodoc: - synchronize do - @connections.each do |connection| - connection.verify! + @available.clear + @connections.each do |conn| + @available.add conn end end end def clear_stale_cached_connections! # :nodoc: + reap end - deprecate :clear_stale_cached_connections! + deprecate :clear_stale_cached_connections! => "Please use #reap instead" # Check-out a database connection from the pool, indicating that you want # to use it. You should call #checkin when you no longer need this. @@ -206,30 +351,17 @@ module ActiveRecord # # If all connections are leased and the pool is at capacity (meaning the # number of currently leased connections is greater than or equal to the - # size limit set), an ActiveRecord::PoolFullError exception will be raised. + # size limit set), an ActiveRecord::ConnectionTimeoutError exception will be raised. # # Returns: an AbstractAdapter object. # # Raises: - # - PoolFullError: no connection can be obtained from the pool. + # - ConnectionTimeoutError: no connection can be obtained from the pool. def checkout - # Checkout an available connection synchronize do - # Try to find a connection that hasn't been leased, and lease it - conn = connections.find { |c| c.lease } - - # If all connections were leased, and we have room to expand, - # create a new connection and lease it. - if !conn && connections.size < size - conn = checkout_new_connection - conn.lease - end - - if conn - checkout_and_verify conn - else - raise PoolFullError.new(size, timeout) - end + conn = acquire_connection + conn.lease + checkout_and_verify(conn) end end @@ -243,6 +375,10 @@ module ActiveRecord conn.run_callbacks :checkin do conn.expire end + + release conn + + @available.add conn end end @@ -251,13 +387,13 @@ module ActiveRecord def remove(conn) synchronize do @connections.delete conn + @available.delete conn # FIXME: we might want to store the key on the connection so that removing # from the reserved hash will be a little easier. - thread_id = @reserved_connections.keys.find { |k| - @reserved_connections[k] == conn - } - @reserved_connections.delete thread_id if thread_id + release conn + + @available.add checkout_new_connection if @available.any_waiting? end end @@ -266,7 +402,7 @@ module ActiveRecord # or a thread dies unexpectedly. def reap synchronize do - stale = Time.now - @timeout + stale = Time.now - @dead_connection_timeout connections.dup.each do |conn| remove conn if conn.in_use? && stale > conn.last_use && !conn.active? end @@ -275,12 +411,41 @@ module ActiveRecord private + # Acquire a connection by one of 1) immediately removing one + # from the queue of available connections, 2) creating a new + # connection if the pool is not at capacity, 3) waiting on the + # queue for a connection to become available. + # + # Raises: + # - ConnectionTimeoutError if a connection could not be acquired + def acquire_connection + if conn = @available.poll + conn + elsif @connections.size < @size + checkout_new_connection + else + @available.poll(@checkout_timeout) + end + end + + def release(conn) + thread_id = if @reserved_connections[current_connection_id] == conn + current_connection_id + else + @reserved_connections.keys.find { |k| + @reserved_connections[k] == conn + } + end + + @reserved_connections.delete thread_id if thread_id + end + def new_connection - ActiveRecord::Base.send(spec.adapter_method, spec.config) + ActiveRecord::Model.send(spec.adapter_method, spec.config) end def current_connection_id #:nodoc: - ActiveRecord::Base.connection_id ||= Thread.current.object_id + ActiveRecord::Model.connection_id ||= Thread.current.object_id end def checkout_new_connection @@ -298,10 +463,6 @@ module ActiveRecord end c end - - def active_connections - @connections.find_all { |c| c.in_use? } - end end # ConnectionHandler is a collection of ConnectionPool objects. It is used @@ -326,47 +487,42 @@ module ActiveRecord # # Normally there is only a single ConnectionHandler instance, accessible via # ActiveRecord::Base.connection_handler. Active Record models use this to - # determine that connection pool that they should use. + # determine the connection pool that they should use. class ConnectionHandler - def initialize(pools = Hash.new { |h,k| h[k] = {} }) - @connection_pools = pools - @class_to_pool = Hash.new { |h,k| h[k] = {} } + def initialize + @owner_to_pool = Hash.new { |h,k| h[k] = {} } + @class_to_pool = Hash.new { |h,k| h[k] = {} } end def connection_pools - @connection_pools[Process.pid] + owner_to_pool.values.compact end - def establish_connection(name, spec) - set_pool_for_spec spec, ConnectionAdapters::ConnectionPool.new(spec) - set_class_to_pool name, connection_pools[spec] + def establish_connection(owner, spec) + @class_to_pool.clear + owner_to_pool[owner] = ConnectionAdapters::ConnectionPool.new(spec) end # Returns true if there are any active connections among the connection # pools that the ConnectionHandler is managing. def active_connections? - connection_pools.values.any? { |pool| pool.active_connection? } + connection_pools.any?(&:active_connection?) end # Returns any connections in use by the current thread back to the pool, # and also returns connections to the pool cached by threads that are no # longer alive. def clear_active_connections! - connection_pools.each_value {|pool| pool.release_connection } + connection_pools.each(&:release_connection) end # Clears the cache which maps classes. def clear_reloadable_connections! - connection_pools.each_value {|pool| pool.clear_reloadable_connections! } + connection_pools.each(&:clear_reloadable_connections!) end def clear_all_connections! - connection_pools.each_value {|pool| pool.disconnect! } - end - - # Verify active connections. - def verify_active_connections! #:nodoc: - connection_pools.each_value {|pool| pool.verify_active_connections! } + connection_pools.each(&:disconnect!) end # Locate the connection of the nearest super class. This can be an @@ -389,54 +545,62 @@ module ActiveRecord # connection and the defined connection (if they exist). The result # can be used as an argument for establish_connection, for easily # re-establishing the connection. - def remove_connection(klass) - pool = class_to_pool.delete(klass.name) - return nil unless pool - - connection_pools.delete pool.spec - pool.automatic_reconnect = false - pool.disconnect! - pool.spec.config + def remove_connection(owner) + if pool = owner_to_pool.delete(owner) + @class_to_pool.clear + pool.automatic_reconnect = false + pool.disconnect! + pool.spec.config + end end + # Retrieving the connection pool happens a lot so we cache it in @class_to_pool. + # This makes retrieving the connection pool O(1) once the process is warm. + # When a connection is established or removed, we invalidate the cache. + # + # Ideally we would use #fetch here, as class_to_pool[klass] may sometimes be nil. + # However, benchmarking (https://gist.github.com/3552829) showed that #fetch is + # significantly slower than #[]. So in the nil case, no caching will take place, + # but that's ok since the nil case is not the common one that we wish to optimise + # for. def retrieve_connection_pool(klass) - pool = get_pool_for_class klass.name - return pool if pool - return nil if ActiveRecord::Model == klass - retrieve_connection_pool klass.active_record_super + class_to_pool[klass] ||= begin + until pool = pool_for(klass) + klass = klass.superclass + break unless klass < ActiveRecord::Tag + end + + class_to_pool[klass] = pool || pool_for(ActiveRecord::Model) + end end private - def class_to_pool - @class_to_pool[Process.pid] - end - - def set_pool_for_spec(spec, pool) - @connection_pools[Process.pid][spec] = pool + def owner_to_pool + @owner_to_pool[Process.pid] end - def set_class_to_pool(name, pool) - @class_to_pool[Process.pid][name] = pool - pool + def class_to_pool + @class_to_pool[Process.pid] end - def get_pool_for_class(klass) - @class_to_pool[Process.pid].fetch(klass) { - c_to_p = @class_to_pool.values.find { |class_to_pool| - class_to_pool[klass] - } - - if c_to_p - pool = c_to_p[klass] - pool = ConnectionAdapters::ConnectionPool.new pool.spec - set_pool_for_spec pool.spec, pool - set_class_to_pool klass, pool + def pool_for(owner) + owner_to_pool.fetch(owner) { + if ancestor_pool = pool_from_any_process_for(owner) + # A connection was established in an ancestor process that must have + # subsequently forked. We can't reuse the connection, but we can copy + # the specification and establish a new connection with it. + establish_connection owner, ancestor_pool.spec else - set_class_to_pool klass, nil + owner_to_pool[owner] = nil end } end + + def pool_from_any_process_for(owner) + owner_to_pool = @owner_to_pool.values.find { |v| v[owner] } + owner_to_pool && owner_to_pool[owner] + end end class ConnectionManagement 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 db99c3fbef..32e3c7f5d8 100644 --- a/activerecord/lib/active_record/connection_adapters/abstract/database_statements.rb +++ b/activerecord/lib/active_record/connection_adapters/abstract/database_statements.rb @@ -1,9 +1,15 @@ module ActiveRecord module ConnectionAdapters # :nodoc: module DatabaseStatements + def initialize + super + reset_transaction + end + # Converts an arel AST to SQL def to_sql(arel, binds = []) if arel.respond_to?(:ast) + binds = binds.dup visitor.accept(arel.ast) do quote(*binds.shift.reverse) end @@ -20,14 +26,14 @@ module ActiveRecord # Returns a record hash with the column names as keys and column values # as values. - def select_one(arel, name = nil) - result = select_all(arel, name) + def select_one(arel, name = nil, binds = []) + result = select_all(arel, name, binds) result.first if result end # Returns a single value from a record - def select_value(arel, name = nil) - if result = select_one(arel, name) + def select_value(arel, name = nil, binds = []) + if result = select_one(arel, name, binds) result.values.first end end @@ -57,21 +63,21 @@ module ActiveRecord end # Executes insert +sql+ statement in the context of this connection using - # +binds+ as the bind substitutes. +name+ is the logged along with + # +binds+ as the bind substitutes. +name+ is logged along with # the executed +sql+ statement. - def exec_insert(sql, name, binds) + def exec_insert(sql, name, binds, pk = nil, sequence_name = nil) exec_query(sql, name, binds) end # Executes delete +sql+ statement in the context of this connection using - # +binds+ as the bind substitutes. +name+ is the logged along with + # +binds+ as the bind substitutes. +name+ is logged along with # the executed +sql+ statement. def exec_delete(sql, name, binds) exec_query(sql, name, binds) end # Executes update +sql+ statement in the context of this connection using - # +binds+ as the bind substitutes. +name+ is the logged along with + # +binds+ as the bind substitutes. +name+ is logged along with # the executed +sql+ statement. def exec_update(sql, name, binds) exec_query(sql, name, binds) @@ -87,7 +93,7 @@ module ActiveRecord # passed in as +id_value+. def insert(arel, name = nil, pk = nil, id_value = nil, sequence_name = nil, binds = []) sql, binds = sql_for_insert(to_sql(arel, binds), pk, id_value, sequence_name, binds) - value = exec_insert(sql, name, binds) + value = exec_insert(sql, name, binds, pk, sequence_name) id_value || last_inserted_id(value) end @@ -101,20 +107,6 @@ module ActiveRecord exec_delete(to_sql(arel, binds), name, binds) end - # Checks whether there is currently no transaction active. This is done - # by querying the database driver, and does not use the transaction - # house-keeping information recorded by #increment_open_transactions and - # friends. - # - # Returns true if there is no transaction active, false if there is a - # transaction active, and nil if this information is unknown. - # - # Not all adapters supports transaction state introspection. Currently, - # only the PostgreSQL adapter supports this. - def outside_transaction? - nil - end - # Returns +true+ when the connection adapter supports prepared statement # caching, otherwise returns +false+ def supports_statement_cache? @@ -166,82 +158,60 @@ module ActiveRecord def transaction(options = {}) options.assert_valid_keys :requires_new, :joinable - last_transaction_joinable = defined?(@transaction_joinable) ? @transaction_joinable : nil - if options.has_key?(:joinable) - @transaction_joinable = options[:joinable] + if !options[:requires_new] && current_transaction.joinable? + yield else - @transaction_joinable = true + within_new_transaction(options) { yield } end - requires_new = options[:requires_new] || !last_transaction_joinable - - transaction_open = false - @_current_transaction_records ||= [] + rescue ActiveRecord::Rollback + # rollbacks are silently swallowed + end - begin - if block_given? - if requires_new || open_transactions == 0 - if open_transactions == 0 - begin_db_transaction - elsif requires_new - create_savepoint - end - increment_open_transactions - transaction_open = true - @_current_transaction_records.push([]) - end - yield - end - rescue Exception => database_transaction_rollback - if transaction_open && !outside_transaction? - transaction_open = false - decrement_open_transactions - if open_transactions == 0 - rollback_db_transaction - rollback_transaction_records(true) - else - rollback_to_savepoint - rollback_transaction_records(false) - end - end - raise unless database_transaction_rollback.is_a?(ActiveRecord::Rollback) - end + def within_new_transaction(options = {}) #:nodoc: + begin_transaction(options) + yield + rescue Exception => error + rollback_transaction + raise ensure - @transaction_joinable = last_transaction_joinable - - if outside_transaction? - @open_transactions = 0 - elsif transaction_open - decrement_open_transactions - begin - if open_transactions == 0 - commit_db_transaction - commit_transaction_records - else - release_savepoint - save_point_records = @_current_transaction_records.pop - unless save_point_records.blank? - @_current_transaction_records.push([]) if @_current_transaction_records.empty? - @_current_transaction_records.last.concat(save_point_records) - end - end - rescue Exception => database_transaction_rollback - if open_transactions == 0 - rollback_db_transaction - rollback_transaction_records(true) - else - rollback_to_savepoint - rollback_transaction_records(false) - end - raise - end + begin + commit_transaction unless error + rescue Exception + rollback_transaction + raise end end + def current_transaction #:nodoc: + @transaction + end + + def transaction_open? + @transaction.open? + end + + def begin_transaction(options = {}) #:nodoc: + @transaction = @transaction.begin + @transaction.joinable = options.fetch(:joinable, true) + @transaction + end + + def commit_transaction #:nodoc: + @transaction = @transaction.commit + end + + def rollback_transaction #:nodoc: + @transaction = @transaction.rollback + end + + def reset_transaction #:nodoc: + @transaction = ClosedTransaction.new(self) + end + # Register a record with the current transaction so that its after_commit and after_rollback callbacks # can be called. def add_transaction_record(record) - last_batch = @_current_transaction_records.last - last_batch << record if last_batch + @transaction.add_record(record) end # Begins the transaction (and turns off auto-committing). @@ -312,13 +282,27 @@ module ActiveRecord # on mysql (even when aliasing the tables), but mysql allows using JOIN directly in # an UPDATE statement, so in the mysql adapters we redefine this to do that. def join_to_update(update, select) #:nodoc: - subselect = select.clone - subselect.projections = [update.key] + key = update.key + subselect = subquery_for(key, select) + + update.where key.in(subselect) + end + + def join_to_delete(delete, select, key) #:nodoc: + subselect = subquery_for(key, select) - update.where update.key.in(subselect) + delete.where key.in(subselect) end protected + + # Return a subquery for the given key using the join information. + def subquery_for(key, select) + subselect = select.clone + subselect.projections = [key] + subselect + end + # Returns an array of record hashes with the column names as keys and # column values as values. def select(sql, name = nil, binds = []) @@ -341,42 +325,6 @@ module ActiveRecord update_sql(sql, name) 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) - if rollback - records = @_current_transaction_records.flatten - @_current_transaction_records.clear - else - records = @_current_transaction_records.pop - end - - unless records.blank? - records.uniq.each do |record| - begin - record.rolledback!(rollback) - rescue Exception => e - record.logger.error(e) if record.respond_to?(:logger) && record.logger - end - end - end - end - - # Send a commit message to all records after they have been committed. - def commit_transaction_records - records = @_current_transaction_records.flatten - @_current_transaction_records.clear - unless records.blank? - records.uniq.each do |record| - begin - record.committed! - rescue Exception => e - record.logger.error(e) if record.respond_to?(:logger) && record.logger - end - end - end - end - def sql_for_insert(sql, pk, id_value, sequence_name, binds) [sql, binds] end 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 17377bad96..be6fda95b4 100644 --- a/activerecord/lib/active_record/connection_adapters/abstract/query_cache.rb +++ b/activerecord/lib/active_record/connection_adapters/abstract/query_cache.rb @@ -2,7 +2,7 @@ module ActiveRecord module ConnectionAdapters # :nodoc: module QueryCache class << self - def included(base) + def included(base) #:nodoc: dirties_query_cache base, :insert, :update, :delete end @@ -56,7 +56,7 @@ module ActiveRecord end def select_all(arel, name = nil, binds = []) - if @query_cache_enabled + if @query_cache_enabled && !locked?(arel) sql = to_sql(arel, binds) cache_sql(sql, binds) { super(sql, name, binds) } else @@ -65,6 +65,7 @@ module ActiveRecord end private + def cache_sql(sql, binds) result = if @query_cache[sql].key?(binds) @@ -83,6 +84,10 @@ module ActiveRecord result.collect { |row| row.dup } end end + + def locked?(arel) + arel.respond_to?(:locked) && arel.locked + end 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 44ac37c498..60a9eee7c7 100644 --- a/activerecord/lib/active_record/connection_adapters/abstract/quoting.rb +++ b/activerecord/lib/active_record/connection_adapters/abstract/quoting.rb @@ -31,9 +31,10 @@ module ActiveRecord # 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 Numeric, ActiveSupport::Duration then value.to_s when Date, Time then "'#{quoted_date(value)}'" when Symbol then "'#{quote_string(value.to_s)}'" + when Class then "'#{value.to_s}'" else "'#{quote_string(YAML.dump(value))}'" 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 ad2e8634eb..dca355aa93 100644 --- a/activerecord/lib/active_record/connection_adapters/abstract/schema_definitions.rb +++ b/activerecord/lib/active_record/connection_adapters/abstract/schema_definitions.rb @@ -1,4 +1,3 @@ -require 'active_support/core_ext/object/blank' require 'date' require 'set' require 'bigdecimal' @@ -6,6 +5,9 @@ require 'bigdecimal/util' module ActiveRecord module ConnectionAdapters #:nodoc: + # Abstract representation of an index definition on a table. Instances of + # this type are typically created and returned by methods in database + # adapters. e.g. ActiveRecord::ConnectionAdapters::AbstractMysqlAdapter#indexes class IndexDefinition < Struct.new(:table, :name, :unique, :columns, :lengths, :orders, :where) #:nodoc: end @@ -20,7 +22,7 @@ module ActiveRecord end def sql_type - base.type_to_sql(type.to_sym, limit, precision, scale) rescue type + base.type_to_sql(type.to_sym, limit, precision, scale) end def to_sql @@ -62,11 +64,12 @@ module ActiveRecord class TableDefinition # An array of ColumnDefinition objects, representing the column changes # that have been defined. - attr_accessor :columns + attr_accessor :columns, :indexes def initialize(base) @columns = [] @columns_hash = {} + @indexes = {} @base = base end @@ -209,19 +212,22 @@ module ActiveRecord # # 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 - # options, these will be used when creating the <tt>_type</tt> column. So what can be written like this: + # options, these will be used when creating the <tt>_type</tt> column. The <tt>:index</tt> option + # will also create an index, similar to calling <tt>add_index</tt>. So what can be written like this: # # create_table :taggings do |t| # t.integer :tag_id, :tagger_id, :taggable_id # t.string :tagger_type # t.string :taggable_type, :default => 'Photo' # end + # add_index :taggings, :tag_id, :name => 'index_taggings_on_tag_id' + # add_index :taggings, [:tagger_id, :tagger_type] # # Can also be written as follows using references: # # create_table :taggings do |t| - # t.references :tag - # t.references :tagger, :polymorphic => true + # t.references :tag, :index => { :name => 'index_taggings_on_tag_id' } + # t.references :tagger, :polymorphic => true, :index => true # t.references :taggable, :polymorphic => { :default => 'Photo' } # end def column(name, type, options = {}) @@ -253,10 +259,18 @@ module ActiveRecord EOV end + # Adds index options to the indexes hash, keyed by column name + # This is primarily used to track indexes that need to be created after the table + # + # index(:account_id, :name => 'index_projects_on_account_id') + def index(column_name, options = {}) + indexes[column_name] = options + end + # Appends <tt>:datetime</tt> columns <tt>:created_at</tt> and # <tt>:updated_at</tt> to the table. def timestamps(*args) - options = { :null => false }.merge(args.extract_options!) + options = args.extract_options! column(:created_at, :datetime, options) column(:updated_at, :datetime, options) end @@ -264,9 +278,11 @@ module ActiveRecord def references(*args) options = args.extract_options! polymorphic = options.delete(:polymorphic) + index_options = options.delete(:index) args.each do |col| column("#{col}_id", :integer, options) - column("#{col}_type", :string, polymorphic.is_a?(Hash) ? polymorphic : options) unless polymorphic.nil? + column("#{col}_type", :string, polymorphic.is_a?(Hash) ? polymorphic : options) if polymorphic + index(polymorphic ? %w(id type).map { |t| "#{col}_#{t}" } : "#{col}_id", index_options.is_a?(Hash) ? index_options : nil) if index_options end end alias :belongs_to :references @@ -331,7 +347,7 @@ module ActiveRecord # Adds a new column to the named table. # See TableDefinition#column for details of the options you can use. - # ===== Example + # # ====== Creating a simple column # t.column(:name, :string) def column(column_name, type, options = {}) @@ -346,7 +362,6 @@ module ActiveRecord # Adds a new index to the table. +column_name+ can be a single Symbol, or # an Array of Symbols. See SchemaStatements#add_index # - # ===== Examples # ====== Creating a simple index # t.index(:name) # ====== Creating a unique index @@ -363,7 +378,7 @@ module ActiveRecord end # Adds timestamps (+created_at+ and +updated_at+) columns to the table. See SchemaStatements#add_timestamps - # ===== Example + # # t.timestamps def timestamps @base.add_timestamps(@table_name) @@ -371,7 +386,7 @@ module ActiveRecord # Changes the column's definition according to the new options. # See TableDefinition#column for details of the options you can use. - # ===== Examples + # # t.change(:name, :string, :limit => 80) # t.change(:description, :text) def change(column_name, type, options = {}) @@ -379,7 +394,7 @@ module ActiveRecord end # Sets a new default value for a column. See SchemaStatements#change_column_default - # ===== Examples + # # t.change_default(:qualification, 'new') # t.change_default(:authorized, 1) def change_default(column_name, default) @@ -387,16 +402,15 @@ module ActiveRecord end # Removes the column(s) from the table definition. - # ===== Examples + # # t.remove(:qualification) # t.remove(:qualification, :experience) def remove(*column_names) - @base.remove_column(@table_name, column_names) + @base.remove_column(@table_name, *column_names) end # Removes the given index from the table. # - # ===== Examples # ====== Remove the index_table_name_on_column in the table_name table # t.remove_index :column # ====== Remove the index named index_table_name_on_branch_id in the table_name table @@ -410,14 +424,14 @@ module ActiveRecord end # Removes the timestamp columns (+created_at+ and +updated_at+) from the table. - # ===== Example + # # t.remove_timestamps def remove_timestamps @base.remove_timestamps(@table_name) end # Renames a column. - # ===== Example + # # t.rename(:description, :name) def rename(column_name, new_column_name) @base.rename_column(@table_name, column_name, new_column_name) @@ -425,38 +439,34 @@ module ActiveRecord # Adds a reference. Optionally adds a +type+ column, if <tt>:polymorphic</tt> option is provided. # <tt>references</tt> and <tt>belongs_to</tt> are acceptable. - # ===== Examples - # t.references(:goat) - # t.references(:goat, :polymorphic => true) - # t.belongs_to(:goat) + # + # t.references(:user) + # t.belongs_to(:supplier, polymorphic: true) + # def references(*args) options = args.extract_options! - polymorphic = options.delete(:polymorphic) - args.each do |col| - @base.add_column(@table_name, "#{col}_id", :integer, options) - @base.add_column(@table_name, "#{col}_type", :string, polymorphic.is_a?(Hash) ? polymorphic : options) unless polymorphic.nil? + args.each do |ref_name| + @base.add_reference(@table_name, ref_name, options) end end alias :belongs_to :references # Removes a reference. Optionally removes a +type+ column. # <tt>remove_references</tt> and <tt>remove_belongs_to</tt> are acceptable. - # ===== Examples - # t.remove_references(:goat) - # t.remove_references(:goat, :polymorphic => true) - # t.remove_belongs_to(:goat) + # + # t.remove_references(:user) + # t.remove_belongs_to(:supplier, polymorphic: true) + # def remove_references(*args) options = args.extract_options! - polymorphic = options.delete(:polymorphic) - args.each do |col| - @base.remove_column(@table_name, "#{col}_id") - @base.remove_column(@table_name, "#{col}_type") unless polymorphic.nil? + args.each do |ref_name| + @base.remove_reference(@table_name, ref_name, options) end end - alias :remove_belongs_to :remove_references + alias :remove_belongs_to :remove_references # Adds a column or columns of a specified type - # ===== Examples + # # t.string(:goat) # t.string(:goat, :sheep) %w( string text integer float decimal datetime timestamp time date binary boolean ).each do |column_type| diff --git a/activerecord/lib/active_record/connection_adapters/abstract/schema_dumper.rb b/activerecord/lib/active_record/connection_adapters/abstract/schema_dumper.rb new file mode 100644 index 0000000000..9d6111b51e --- /dev/null +++ b/activerecord/lib/active_record/connection_adapters/abstract/schema_dumper.rb @@ -0,0 +1,56 @@ +module ActiveRecord + module ConnectionAdapters # :nodoc: + # The goal of this module is to move Adapter specific column + # definitions to the Adapter instead of having it in the schema + # dumper itself. This code represents the normal case. + # We can then redefine how certain data types may be handled in the schema dumper on the + # Adapter level by over-writing this code inside the database spececific adapters + module ColumnDumper + def column_spec(column, types) + spec = prepare_column_options(column, types) + (spec.keys - [:name, :type]).each{ |k| spec[k].insert(0, "#{k.to_s}: ")} + spec + end + + # This can be overridden on a Adapter level basis to support other + # extended datatypes (Example: Adding an array option in the + # PostgreSQLAdapter) + def prepare_column_options(column, types) + spec = {} + spec[:name] = column.name.inspect + + # AR has an optimization which handles zero-scale decimals as integers. This + # code ensures that the dumper still dumps the column as a decimal. + spec[:type] = if column.type == :integer && /^(numeric|decimal)/ =~ column.sql_type + 'decimal' + else + 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 + 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 + end + + # Lists the valid migration options + def migration_keys + [:name, :limit, :precision, :scale, :default, :null] + end + + private + + def default_string(value) + case value + when BigDecimal + value.to_s + when Date, DateTime, Time + "'#{value.to_s(:db)}'" + else + value.inspect + end + end + end + end +end diff --git a/activerecord/lib/active_record/connection_adapters/abstract/schema_statements.rb b/activerecord/lib/active_record/connection_adapters/abstract/schema_statements.rb index 6bac05191d..86d6266af9 100644 --- a/activerecord/lib/active_record/connection_adapters/abstract/schema_statements.rb +++ b/activerecord/lib/active_record/connection_adapters/abstract/schema_statements.rb @@ -1,5 +1,3 @@ -require 'active_support/deprecation/reporting' -require 'active_record/schema_migration' require 'active_record/migration/join_table' module ActiveRecord @@ -16,12 +14,11 @@ 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].gsub(/\./, '_') + table_name[0...table_alias_length].tr('.', '_') end # Checks to see if the table +table_name+ exists on the database. # - # === Example # table_exists?(:developers) def table_exists?(table_name) tables.include?(table_name.to_s) @@ -32,7 +29,6 @@ module ActiveRecord # Checks to see if an index exists on a table for a given index definition. # - # === Examples # # Check an index exists # index_exists?(:suppliers, :company_id) # @@ -60,7 +56,6 @@ module ActiveRecord # Checks to see if a column exists in a given table. # - # === Examples # # Check a column exists # column_exists?(:suppliers, :name) # @@ -68,13 +63,18 @@ module ActiveRecord # column_exists?(:suppliers, :name, :string) # # # Check a column exists with a specific definition - # column_exists?(:suppliers, :name, :string, :limit => 100) + # column_exists?(:suppliers, :name, :string, limit: 100) + # column_exists?(:suppliers, :name, :string, default: 'default') + # column_exists?(:suppliers, :name, :string, null: false) + # column_exists?(:suppliers, :tax, :decimal, precision: 8, scale: 2) def column_exists?(table_name, column_name, type = nil, options = {}) columns(table_name).any?{ |c| c.name == column_name.to_s && - (!type || c.type == type) && - (!options[:limit] || c.limit == options[:limit]) && - (!options[:precision] || c.precision == options[:precision]) && - (!options[:scale] || c.scale == options[:scale]) } + (!type || c.type == type) && + (!options.key?(:limit) || c.limit == options[:limit]) && + (!options.key?(:precision) || c.precision == options[:precision]) && + (!options.key?(:scale) || c.scale == options[:scale]) && + (!options.key?(:default) || c.default == options[:default]) && + (!options.key?(:null) || c.null == options[:null]) } end # Creates a new table with the name +table_name+. +table_name+ may either @@ -126,7 +126,6 @@ module ActiveRecord # Set to true to drop the table before creating it. # Defaults to false. # - # ===== Examples # ====== Add a backend specific option to the generated SQL (MySQL) # create_table(:suppliers, :options => 'ENGINE=InnoDB DEFAULT CHARSET=utf8') # generates: @@ -171,10 +170,11 @@ module ActiveRecord create_sql << td.to_sql create_sql << ") #{options[:options]}" execute create_sql + td.indexes.each_pair { |c,o| add_index table_name, c, o } end # Creates a new join table with the name created using the lexical order of the first two - # arguments. These arguments can be be a String or a Symbol. + # arguments. These arguments can be a String or a Symbol. # # # Creates a table called 'assemblies_parts' with no id. # create_join_table(:assemblies, :parts) @@ -192,7 +192,6 @@ module ActiveRecord # Set to true to drop the table before creating it. # Defaults to false. # - # ===== Examples # ====== Add a backend specific option to the generated SQL (MySQL) # create_join_table(:assemblies, :parts, :options => 'ENGINE=InnoDB DEFAULT CHARSET=utf8') # generates: @@ -204,17 +203,19 @@ module ActiveRecord join_table_name = find_join_table_name(table_1, table_2, options) column_options = options.delete(:column_options) || {} - column_options.reverse_merge!({:null => false}) + column_options.reverse_merge!(null: false) - create_table(join_table_name, options.merge!(:id => false)) do |td| - td.integer :"#{table_1.to_s.singularize}_id", column_options - td.integer :"#{table_2.to_s.singularize}_id", column_options + t1_column, t2_column = [table_1, table_2].map{ |t| t.to_s.singularize.foreign_key } + + create_table(join_table_name, options.merge!(id: false)) do |td| + td.integer t1_column, column_options + td.integer t2_column, column_options + yield td if block_given? end end # A block for changing columns in +table+. # - # === Example # # change_table() yields a Table instance # change_table(:suppliers) do |t| # t.column :name, :string, :limit => 60 @@ -228,7 +229,6 @@ module ActiveRecord # # Defaults to false. # - # ===== Examples # ====== Add a column # change_table(:suppliers) do |t| # t.column :name, :string, :limit => 60 @@ -287,7 +287,7 @@ module ActiveRecord end # Renames a table. - # ===== Example + # # rename_table('octopuses', 'octopi') def rename_table(table_name, new_name) raise NotImplementedError, "rename_table is not implemented" @@ -307,7 +307,7 @@ module ActiveRecord end # Removes the column(s) from the table definition. - # ===== Examples + # # remove_column(:suppliers, :qualification) # remove_columns(:suppliers, :qualification, :experience) def remove_column(table_name, *column_names) @@ -317,7 +317,7 @@ module ActiveRecord # Changes the column's definition according to the new options. # See TableDefinition#column for details of the options you can use. - # ===== Examples + # # change_column(:suppliers, :name, :string, :limit => 80) # change_column(:accounts, :description, :text) def change_column(table_name, column_name, type, options = {}) @@ -325,7 +325,7 @@ module ActiveRecord end # 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) @@ -334,7 +334,7 @@ module ActiveRecord end # Renames a column. - # ===== Example + # # rename_column(:suppliers, :description, :name) def rename_column(table_name, column_name, new_column_name) raise NotImplementedError, "rename_column is not implemented" @@ -346,8 +346,6 @@ module ActiveRecord # The index will be named after the table and the column name(s), unless # you pass <tt>:name</tt> as an option. # - # ===== Examples - # # ====== Creating a simple index # add_index(:suppliers, :name) # generates @@ -375,7 +373,7 @@ module ActiveRecord # Note: SQLite doesn't support index length # # ====== Creating an index with a sort order (desc or asc, asc is the default) - # add_index(:accounts, [:branch_id, :party_id, :surname], :order => {:branch_id => :desc, :part_id => :asc}) + # add_index(:accounts, [:branch_id, :party_id, :surname], :order => {:branch_id => :desc, :party_id => :asc}) # generates # CREATE INDEX by_branch_desc_party ON accounts(branch_id DESC, party_id ASC, surname) # @@ -447,6 +445,42 @@ module ActiveRecord indexes(table_name).detect { |i| i.name == index_name } end + # Adds a reference. Optionally adds a +type+ column, if <tt>:polymorphic</tt> option is provided. + # <tt>add_reference</tt> and <tt>add_belongs_to</tt> are acceptable. + # + # ====== Create a user_id column + # add_reference(:products, :user) + # + # ====== Create a supplier_id and supplier_type columns + # add_belongs_to(:products, :supplier, polymorphic: true) + # + # ====== Create a supplier_id, supplier_type columns and appropriate index + # add_reference(:products, :supplier, polymorphic: true, index: true) + # + def add_reference(table_name, ref_name, options = {}) + polymorphic = options.delete(:polymorphic) + index_options = options.delete(:index) + add_column(table_name, "#{ref_name}_id", :integer, options) + add_column(table_name, "#{ref_name}_type", :string, polymorphic.is_a?(Hash) ? polymorphic : options) if polymorphic + add_index(table_name, polymorphic ? %w[id type].map{ |t| "#{ref_name}_#{t}" } : "#{ref_name}_id", index_options.is_a?(Hash) ? index_options : nil) if index_options + end + alias :add_belongs_to :add_reference + + # Removes the reference(s). Also removes a +type+ column if one exists. + # <tt>remove_reference</tt>, <tt>remove_references</tt> and <tt>remove_belongs_to</tt> are acceptable. + # + # ====== Remove the reference + # remove_reference(:products, :user, index: true) + # + # ====== Remove polymorphic reference + # remove_reference(:products, :supplier, polymorphic: true) + # + def remove_reference(table_name, ref_name, options = {}) + remove_column(table_name, "#{ref_name}_id") + remove_column(table_name, "#{ref_name}_type") if options[:polymorphic] + end + alias :remove_belongs_to :remove_reference + # Returns a string of <tt>CREATE TABLE</tt> SQL statement(s) for recreating the # entire structure of the database. def structure_dump @@ -455,7 +489,7 @@ module ActiveRecord def dump_schema_information #:nodoc: sm_table = ActiveRecord::Migrator.schema_migrations_table_name - ActiveRecord::SchemaMigration.order('version').all.map { |sm| + ActiveRecord::SchemaMigration.order('version').map { |sm| "INSERT INTO #{sm_table} (version) VALUES ('#{sm.version}');" }.join "\n\n" end @@ -536,15 +570,15 @@ module ActiveRecord end # Adds timestamps (created_at and updated_at) columns to the named table. - # ===== Examples + # # add_timestamps(:suppliers) def add_timestamps(table_name) - add_column table_name, :created_at, :datetime, :null => false - add_column table_name, :updated_at, :datetime, :null => false + add_column table_name, :created_at, :datetime + add_column table_name, :updated_at, :datetime end # Removes the timestamp columns (created_at and updated_at) from the table definition. - # ===== Examples + # # remove_timestamps(:suppliers) def remove_timestamps(table_name) remove_column table_name, :updated_at @@ -556,7 +590,7 @@ module ActiveRecord if options.is_a?(Hash) && order = options[:order] case order when Hash - column_names.each {|name| option_strings[name] += " #{order[name].to_s.upcase}" if order.has_key?(name)} + column_names.each {|name| option_strings[name] += " #{order[name].upcase}" if order.has_key?(name)} when String column_names.each {|name| option_strings[name] += " #{order.upcase}"} end @@ -617,8 +651,6 @@ module ActiveRecord 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 diff --git a/activerecord/lib/active_record/connection_adapters/abstract/transaction.rb b/activerecord/lib/active_record/connection_adapters/abstract/transaction.rb new file mode 100644 index 0000000000..2117eae5cb --- /dev/null +++ b/activerecord/lib/active_record/connection_adapters/abstract/transaction.rb @@ -0,0 +1,156 @@ +module ActiveRecord + module ConnectionAdapters + class Transaction #:nodoc: + attr_reader :connection + + def initialize(connection) + @connection = connection + end + end + + class ClosedTransaction < Transaction #:nodoc: + def number + 0 + end + + def begin + RealTransaction.new(connection, self) + end + + def closed? + true + end + + def open? + false + end + + def joinable? + false + end + + # This is a noop when there are no open transactions + def add_record(record) + end + end + + class OpenTransaction < Transaction #:nodoc: + attr_reader :parent, :records + attr_writer :joinable + + def initialize(connection, parent) + super connection + + @parent = parent + @records = [] + @finishing = false + @joinable = true + end + + # This state is necesarry so that we correctly handle stuff that might + # happen in a commit/rollback. But it's kinda distasteful. Maybe we can + # find a better way to structure it in the future. + def finishing? + @finishing + end + + def joinable? + @joinable && !finishing? + end + + def number + if finishing? + parent.number + else + parent.number + 1 + end + end + + def begin + if finishing? + parent.begin + else + SavepointTransaction.new(connection, self) + end + end + + def rollback + @finishing = true + perform_rollback + parent + end + + def commit + @finishing = true + perform_commit + parent + end + + def add_record(record) + records << record + end + + def rollback_records + records.uniq.each do |record| + begin + record.rolledback!(parent.closed?) + rescue => e + record.logger.error(e) if record.respond_to?(:logger) && record.logger + end + end + end + + def commit_records + records.uniq.each do |record| + begin + record.committed! + rescue => e + record.logger.error(e) if record.respond_to?(:logger) && record.logger + end + end + end + + def closed? + false + end + + def open? + true + end + end + + class RealTransaction < OpenTransaction #:nodoc: + def initialize(connection, parent) + super + connection.begin_db_transaction + end + + def perform_rollback + connection.rollback_db_transaction + rollback_records + end + + def perform_commit + connection.commit_db_transaction + commit_records + end + end + + class SavepointTransaction < OpenTransaction #:nodoc: + def initialize(connection, parent) + super + connection.create_savepoint + end + + def perform_rollback + connection.rollback_to_savepoint + rollback_records + end + + def perform_commit + connection.release_savepoint + records.each { |r| parent.add_record(r) } + 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 c6af682d28..3a8fbcf93f 100644 --- a/activerecord/lib/active_record/connection_adapters/abstract_adapter.rb +++ b/activerecord/lib/active_record/connection_adapters/abstract_adapter.rb @@ -2,9 +2,10 @@ require 'date' require 'bigdecimal' require 'bigdecimal/util' require 'active_support/core_ext/benchmark' -require 'active_support/deprecation' require 'active_record/connection_adapters/schema_cache' +require 'active_record/connection_adapters/abstract/schema_dumper' require 'monitor' +require 'active_support/deprecation' module ActiveRecord module ConnectionAdapters # :nodoc: @@ -34,6 +35,12 @@ module ActiveRecord autoload :QueryCache end + autoload_at 'active_record/connection_adapters/abstract/transaction' do + autoload :ClosedTransaction + autoload :RealTransaction + autoload :SavepointTransaction + end + # Active Record supports multiple database systems. AbstractAdapter and # related classes form the abstraction layer which makes this possible. # An AbstractAdapter represents a connection to a database, and provides an @@ -53,23 +60,22 @@ module ActiveRecord include QueryCache include ActiveSupport::Callbacks include MonitorMixin + include ColumnDumper define_callbacks :checkout, :checkin attr_accessor :visitor, :pool - attr_reader :schema_cache, :last_use, :in_use + attr_reader :schema_cache, :last_use, :in_use, :logger alias :in_use? :in_use def initialize(connection, logger = nil, pool = nil) #:nodoc: super() - @active = nil @connection = connection @in_use = false @instrumenter = ActiveSupport::Notifications.instrumenter @last_use = false @logger = logger - @open_transactions = 0 @pool = pool @query_cache = Hash.new { |h,sql| h[sql] = {} } @query_cache_enabled = false @@ -86,6 +92,11 @@ module ActiveRecord end end + def schema_cache=(cache) + cache.connection = self + @schema_cache = cache + end + def expire @in_use = false end @@ -158,11 +169,6 @@ module ActiveRecord # QUOTING ================================================== - # Override to return the quoted table name. Defaults to column quoting. - def quote_table_name(name) - quote_column_name(name) - end - # Returns a bind substitution value given a +column+ and list of current # +binds+ def substitute_at(column, index) @@ -182,19 +188,21 @@ module ActiveRecord # checking whether the database is actually capable of responding, i.e. whether # the connection isn't stale. def active? - @active != false end # Disconnects from the database if already connected, and establishes a - # new connection with the database. + # new connection with the database. Implementors should call super if they + # override the default implementation. def reconnect! - @active = true + clear_cache! + reset_transaction end # Disconnects from the database if already connected. Otherwise, this # method does nothing. def disconnect! - @active = false + clear_cache! + reset_transaction end # Reset the state of this connection, directing the DBMS to clear @@ -237,18 +245,21 @@ module ActiveRecord @connection end - attr_reader :open_transactions + def open_transactions + @transaction.number + end def increment_open_transactions - @open_transactions += 1 + ActiveSupport::Deprecation.warn "#increment_open_transactions is deprecated and has no effect" end def decrement_open_transactions - @open_transactions -= 1 + ActiveSupport::Deprecation.warn "#decrement_open_transactions is deprecated and has no effect" end def transaction_joinable=(joinable) - @transaction_joinable = joinable + ActiveSupport::Deprecation.warn "#transaction_joinable= is deprecated. Please pass the :joinable option to #begin_transaction instead." + @transaction.joinable = joinable end def create_savepoint @@ -279,26 +290,25 @@ module ActiveRecord protected - 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 - exception = translate_exception(e, message) - exception.set_backtrace e.backtrace - raise exception - end - - def translate_exception(e, message) - # override in derived class - ActiveRecord::StatementInvalid.new(message) - end - + def log(sql, name = "SQL", binds = []) + @instrumenter.instrument( + "sql.active_record", + :sql => sql, + :name => name, + :connection_id => object_id, + :binds => binds) { yield } + rescue => e + message = "#{e.class.name}: #{e.message}: #{sql}" + @logger.error message if @logger + exception = translate_exception(e, message) + exception.set_backtrace e.backtrace + raise exception + end + + def translate_exception(exception, message) + # override in derived class + ActiveRecord::StatementInvalid.new(message) + end end end end diff --git a/activerecord/lib/active_record/connection_adapters/abstract_mysql_adapter.rb b/activerecord/lib/active_record/connection_adapters/abstract_mysql_adapter.rb index eec7efadc2..1126fe7fce 100644 --- a/activerecord/lib/active_record/connection_adapters/abstract_mysql_adapter.rb +++ b/activerecord/lib/active_record/connection_adapters/abstract_mysql_adapter.rb @@ -1,4 +1,3 @@ -require 'active_support/core_ext/object/blank' require 'arel/visitors/bind_visitor' module ActiveRecord @@ -72,6 +71,8 @@ module ActiveRecord when /^mediumint/i; 3 when /^smallint/i; 2 when /^tinyint/i; 1 + when /^enum\((.+)\)/i + $1.split(',').map{|enum| enum.strip.length - 2}.max else super end @@ -251,7 +252,7 @@ module ActiveRecord end # MysqlAdapter has to free a result after using it, so we use this method to write - # stuff in a abstract way without concerning ourselves about whether it needs to be + # stuff in an abstract way without concerning ourselves about whether it needs to be # explicitly freed or not. def execute_and_free(sql, name = nil) #:nodoc: yield execute(sql, name) @@ -264,19 +265,19 @@ module ActiveRecord def begin_db_transaction execute "BEGIN" - rescue Exception + rescue # Transactions aren't supported end def commit_db_transaction #:nodoc: execute "COMMIT" - rescue Exception + rescue # Transactions aren't supported end def rollback_db_transaction #:nodoc: execute "ROLLBACK" - rescue Exception + rescue # Transactions aren't supported end @@ -294,19 +295,10 @@ module ActiveRecord # In the simple case, MySQL allows us to place JOINs directly into the UPDATE # query. However, this does not allow for LIMIT, OFFSET and ORDER. To support - # these, we must use a subquery. However, MySQL is too stupid to create a - # temporary table for this automatically, so we have to give it some prompting - # in the form of a subsubquery. Ugh! + # these, we must use a subquery. def join_to_update(update, select) #:nodoc: if select.limit || select.offset || select.orders.any? - subsubselect = select.clone - subsubselect.projections = [update.key] - - subselect = Arel::SelectManager.new(select.engine) - subselect.project Arel.sql(update.key.name) - subselect.from subsubselect.as('__active_record_temp') - - update.where update.key.in(subselect) + super else update.table select.source update.wheres = select.constraints @@ -322,10 +314,10 @@ module ActiveRecord sql = "SHOW TABLES" end - select_all(sql).map { |table| + select_all(sql, 'SCHEMA').map { |table| table.delete('Table_type') sql = "SHOW CREATE TABLE #{quote_table_name(table.to_a.first.last)}" - exec_without_stmt(sql).first['Create Table'] + ";\n\n" + exec_query(sql, 'SCHEMA').first['Create Table'] + ";\n\n" }.join end @@ -375,7 +367,7 @@ module ActiveRecord def tables(name = nil, database = nil, like = nil) #:nodoc: sql = "SHOW TABLES " - sql << "IN #{database} " if database + sql << "IN #{quote_table_name(database)} " if database sql << "LIKE #{quote(like)}" if like execute_and_free(sql, 'SCHEMA') do |result| @@ -484,15 +476,26 @@ module ActiveRecord # Maps logical Rails types to MySQL-specific data types. def type_to_sql(type, limit = nil, precision = nil, scale = nil) - return super unless type.to_s == 'integer' - - case limit - when 1; 'tinyint' - when 2; 'smallint' - when 3; 'mediumint' - when nil, 4, 11; 'int(11)' # compatibility with MySQL default - when 5..8; 'bigint' - else raise(ActiveRecordError, "No integer type has byte size #{limit}") + case type.to_s + when 'integer' + case limit + when 1; 'tinyint' + when 2; 'smallint' + when 3; 'mediumint' + when nil, 4, 11; 'int(11)' # compatibility with MySQL default + when 5..8; 'bigint' + else raise(ActiveRecordError, "No integer type has byte size #{limit}") + end + when 'text' + case limit + when 0..0xff; 'tinytext' + when nil, 0x100..0xffff; 'text' + when 0x10000..0xffffff; 'mediumtext' + when 0x1000000..0xffffffff; 'longtext' + else raise(ActiveRecordError, "No text type has character length #{limit}") + end + else + super end end @@ -506,7 +509,7 @@ module ActiveRecord # SHOW VARIABLES LIKE 'name' def show_variable(name) - variables = select_all("SHOW VARIABLES LIKE '#{name}'") + variables = select_all("SHOW VARIABLES LIKE '#{name}'", 'SCHEMA') variables.first['Value'] unless variables.empty? end @@ -514,8 +517,8 @@ module ActiveRecord def pk_and_sequence_for(table) execute_and_free("SHOW CREATE TABLE #{quote_table_name(table)}", 'SCHEMA') do |result| create_table = each_hash(result).first[:"Create Table"] - if create_table.to_s =~ /PRIMARY KEY\s+\((.+)\)/ - keys = $1.split(",").map { |key| key.gsub(/[`"]/, "") } + if create_table.to_s =~ /PRIMARY KEY\s+(?:USING\s+\w+\s+)?\((.+)\)/ + keys = $1.split(",").map { |key| key.delete('`"') } keys.length == 1 ? [keys.first, nil] : nil else nil @@ -547,6 +550,17 @@ module ActiveRecord protected + # MySQL is too stupid to create a temporary table for use subquery, so we have + # to give it some prompting in the form of a subsubquery. Ugh! + def subquery_for(key, select) + subsubselect = select.clone + subsubselect.projections = [key] + + subselect = Arel::SelectManager.new(select.engine) + subselect.project Arel.sql(key.name) + subselect.from subsubselect.as('__active_record_temp') + end + def add_index_length(option_strings, column_names, options = {}) if options.is_a?(Hash) && length = options[:length] case length @@ -617,7 +631,7 @@ module ActiveRecord 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"] + current_type = select_one("SHOW COLUMNS FROM #{quote_table_name(table_name)} LIKE '#{column_name}'", 'SCHEMA')["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 diff --git a/activerecord/lib/active_record/connection_adapters/column.rb b/activerecord/lib/active_record/connection_adapters/column.rb index 78e54c4c9b..816b5e17c1 100644 --- a/activerecord/lib/active_record/connection_adapters/column.rb +++ b/activerecord/lib/active_record/connection_adapters/column.rb @@ -94,7 +94,7 @@ module ActiveRecord case type when :string, :text then value - when :integer then value.to_i rescue value ? 1 : 0 + when :integer then value.to_i when :float then value.to_f when :decimal then klass.value_to_decimal(value) when :datetime, :timestamp then klass.string_to_time(value) @@ -107,11 +107,14 @@ module ActiveRecord end def type_cast_code(var_name) + ActiveSupport::Deprecation.warn("Column#type_cast_code is deprecated in favor of" \ + "using Column#type_cast only, and it is going to be removed in future Rails versions.") + 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 :integer then "(#{var_name}.to_i)" 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})" @@ -120,6 +123,8 @@ module ActiveRecord when :binary then "#{klass}.binary_to_string(#{var_name})" when :boolean then "#{klass}.value_to_boolean(#{var_name})" when :hstore then "#{klass}.string_to_hstore(#{var_name})" + when :inet, :cidr then "#{klass}.string_to_cidr(#{var_name})" + when :json then "#{klass}.string_to_json(#{var_name})" else var_name end end @@ -154,7 +159,7 @@ module ActiveRecord def value_to_date(value) if value.is_a?(String) - return nil if value.empty? + return nil if value.blank? fast_string_to_date(value) || fallback_string_to_date(value) elsif value.respond_to?(:to_date) value.to_date @@ -165,16 +170,22 @@ module ActiveRecord def string_to_time(string) return string unless string.is_a?(String) - return nil if string.empty? + return nil if string.blank? 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? + return nil if string.blank? + + dummy_time_string = "2000-01-01 #{string}" - string_to_time "2000-01-01 #{string}" + fast_string_to_time(dummy_time_string) || begin + time_hash = Date._parse(dummy_time_string) + return nil if time_hash[:hour].nil? + new_time(*time_hash.values_at(:year, :mon, :mday, :hour, :min, :sec, :sec_fraction)) + end end # convert something to a boolean @@ -204,7 +215,7 @@ module ActiveRecord # '0.123456' -> 123456 # '1.123456' -> 123456 def microseconds(time) - ((time[:sec_fraction].to_f % 1) * 1_000_000).to_i + time[:sec_fraction] ? (time[:sec_fraction] * 1_000_000).to_i : 0 end def new_date(year, mon, mday) @@ -229,7 +240,7 @@ module ActiveRecord # 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 + microsec = ($7.to_r * 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 diff --git a/activerecord/lib/active_record/connection_adapters/connection_specification.rb b/activerecord/lib/active_record/connection_adapters/connection_specification.rb index 8491d42b86..b9a61f7d91 100644 --- a/activerecord/lib/active_record/connection_adapters/connection_specification.rb +++ b/activerecord/lib/active_record/connection_adapters/connection_specification.rb @@ -1,3 +1,5 @@ +require 'uri' + module ActiveRecord module ConnectionAdapters class ConnectionSpecification #:nodoc: @@ -70,7 +72,7 @@ module ActiveRecord :port => config.port, :database => config.path.sub(%r{^/},""), :host => config.host } - spec.reject!{ |_,value| !value } + spec.reject!{ |_,value| value.blank? } if config.query options = Hash[config.query.split("&").map{ |pair| pair.split("=") }].symbolize_keys spec.merge!(options) diff --git a/activerecord/lib/active_record/connection_adapters/mysql2_adapter.rb b/activerecord/lib/active_record/connection_adapters/mysql2_adapter.rb index 3f45f23de8..328d080687 100644 --- a/activerecord/lib/active_record/connection_adapters/mysql2_adapter.rb +++ b/activerecord/lib/active_record/connection_adapters/mysql2_adapter.rb @@ -74,6 +74,7 @@ module ActiveRecord end def reconnect! + super disconnect! connect end @@ -82,6 +83,7 @@ module ActiveRecord # Disconnects from the database if already connected. # Otherwise, this method does nothing. def disconnect! + super unless @connection.nil? @connection.close @connection = nil @@ -226,7 +228,7 @@ module ActiveRecord end alias :create :insert_sql - def exec_insert(sql, name, binds) + def exec_insert(sql, name, binds, pk = nil, sequence_name = nil) execute to_sql(sql, binds), name end @@ -253,6 +255,14 @@ module ActiveRecord # By default, MySQL 'where id is null' selects the last inserted id. # Turn this off. http://dev.rubyonrails.org/ticket/6778 variable_assignments = ['SQL_AUTO_IS_NULL=0'] + + # Make MySQL reject illegal values rather than truncating or + # blanking them. See + # http://dev.mysql.com/doc/refman/5.5/en/server-sql-mode.html#sqlmode_strict_all_tables + if @config.fetch(:strict, true) + variable_assignments << "SQL_MODE='STRICT_ALL_TABLES'" + end + encoding = @config[:encoding] # make sure we set the encoding @@ -260,7 +270,7 @@ module ActiveRecord # increase timeout so mysql server doesn't disconnect us wait_timeout = @config[:wait_timeout] - wait_timeout = 2592000 unless wait_timeout.is_a?(Fixnum) + wait_timeout = 2147483 unless wait_timeout.is_a?(Fixnum) variable_assignments << "@@wait_timeout = #{wait_timeout}" execute("SET #{variable_assignments.join(', ')}", :skip_logging) diff --git a/activerecord/lib/active_record/connection_adapters/mysql_adapter.rb b/activerecord/lib/active_record/connection_adapters/mysql_adapter.rb index 724dbff1f0..0b936bbf39 100644 --- a/activerecord/lib/active_record/connection_adapters/mysql_adapter.rb +++ b/activerecord/lib/active_record/connection_adapters/mysql_adapter.rb @@ -7,8 +7,6 @@ require 'mysql' class Mysql class Time - ### - # This monkey patch is for test_additional_columns_from_join_table def to_date Date.new(year, month, day) end @@ -53,6 +51,7 @@ module ActiveRecord # * <tt>:database</tt> - The name of the database. No default, must be provided. # * <tt>:encoding</tt> - (Optional) Sets the client encoding by executing "SET NAMES <encoding>" after connection. # * <tt>:reconnect</tt> - Defaults to false (See MySQL documentation: http://dev.mysql.com/doc/refman/5.0/en/auto-reconnect.html). + # * <tt>:strict</tt> - Defaults to true. Enable STRICT_ALL_TABLES. (See MySQL documentation: http://dev.mysql.com/doc/refman/5.5/en/server-sql-mode.html) # * <tt>:sslca</tt> - Necessary to use MySQL with an SSL connection. # * <tt>:sslkey</tt> - Necessary to use MySQL with an SSL connection. # * <tt>:sslcert</tt> - Necessary to use MySQL with an SSL connection. @@ -190,14 +189,15 @@ module ActiveRecord end def reconnect! + super disconnect! - clear_cache! connect end # Disconnects from the database if already connected. Otherwise, this # method does nothing. def disconnect! + super @connection.close rescue nil end @@ -214,7 +214,7 @@ module ActiveRecord def select_rows(sql, name = nil) @connection.query_with_result = true - rows = exec_without_stmt(sql, name).rows + rows = exec_query(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 @@ -278,31 +278,164 @@ module ActiveRecord end def exec_query(sql, name = 'SQL', binds = []) - log(sql, name, binds) do - exec_stmt(sql, name, binds) do |cols, stmt| - ActiveRecord::Result.new(cols, stmt.to_a) if cols - end + # If the configuration sets prepared_statements:false, binds will + # always be empty, since the bind variables will have been already + # substituted and removed from binds by BindVisitor, so this will + # effectively disable prepared statement usage completely. + if binds.empty? + result_set, affected_rows = exec_without_stmt(sql, name) + else + result_set, affected_rows = exec_stmt(sql, name, binds) end + + yield affected_rows if block_given? + + result_set end def last_inserted_id(result) @connection.insert_id end + module Fields + class Type + def type; end + + def type_cast_for_write(value) + value + end + end + + class Identity < Type + def type_cast(value); value; end + end + + class Integer < Type + def type_cast(value) + return if value.nil? + + value.to_i rescue value ? 1 : 0 + end + end + + class Date < Type + def type; :date; end + + def type_cast(value) + return if value.nil? + + # FIXME: probably we can improve this since we know it is mysql + # specific + ConnectionAdapters::Column.value_to_date value + end + end + + class DateTime < Type + def type; :datetime; end + + def type_cast(value) + return if value.nil? + + # FIXME: probably we can improve this since we know it is mysql + # specific + ConnectionAdapters::Column.string_to_time value + end + end + + class Time < Type + def type; :time; end + + def type_cast(value) + return if value.nil? + + # FIXME: probably we can improve this since we know it is mysql + # specific + ConnectionAdapters::Column.string_to_dummy_time value + end + end + + class Float < Type + def type; :float; end + + def type_cast(value) + return if value.nil? + + value.to_f + end + end + + class Decimal < Type + def type_cast(value) + return if value.nil? + + ConnectionAdapters::Column.value_to_decimal value + end + end + + class Boolean < Type + def type_cast(value) + return if value.nil? + + ConnectionAdapters::Column.value_to_boolean value + end + end + + TYPES = {} + + # Register an MySQL +type_id+ with a typcasting object in + # +type+. + def self.register_type(type_id, type) + TYPES[type_id] = type + end + + def self.alias_type(new, old) + TYPES[new] = TYPES[old] + end + + register_type Mysql::Field::TYPE_TINY, Fields::Boolean.new + register_type Mysql::Field::TYPE_LONG, Fields::Integer.new + alias_type Mysql::Field::TYPE_LONGLONG, Mysql::Field::TYPE_LONG + alias_type Mysql::Field::TYPE_NEWDECIMAL, Mysql::Field::TYPE_LONG + + register_type Mysql::Field::TYPE_VAR_STRING, Fields::Identity.new + register_type Mysql::Field::TYPE_BLOB, Fields::Identity.new + register_type Mysql::Field::TYPE_DATE, Fields::Date.new + register_type Mysql::Field::TYPE_DATETIME, Fields::DateTime.new + register_type Mysql::Field::TYPE_TIME, Fields::Time.new + register_type Mysql::Field::TYPE_FLOAT, Fields::Float.new + + Mysql::Field.constants.grep(/TYPE/).map { |class_name| + Mysql::Field.const_get class_name + }.reject { |const| TYPES.key? const }.each do |const| + register_type const, Fields::Identity.new + 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 = [] + affected_rows = @connection.affected_rows if result - cols = result.fetch_fields.map { |field| field.name } - rows = result.to_a + types = {} + result.fetch_fields.each { |field| + if field.decimals > 0 + types[field.name] = Fields::Decimal.new + else + types[field.name] = Fields::TYPES.fetch(field.type) { + Fields::Identity.new + } + end + } + result_set = ActiveRecord::Result.new(types.keys, result.to_a, types) result.free + else + result_set = ActiveRecord::Result.new([], []) end - ActiveRecord::Result.new(cols, rows) + + [result_set, affected_rows] end end @@ -320,16 +453,18 @@ module ActiveRecord alias :create :insert_sql def exec_delete(sql, name, binds) - log(sql, name, binds) do - exec_stmt(sql, name, binds) do |cols, stmt| - stmt.affected_rows - end + affected_rows = 0 + + exec_query(sql, name, binds) do |n| + affected_rows = n end + + affected_rows end alias :exec_update :exec_delete def begin_db_transaction #:nodoc: - exec_without_stmt "BEGIN" + exec_query "BEGIN" rescue Mysql::Error # Transactions aren't supported end @@ -338,41 +473,44 @@ module ActiveRecord def exec_stmt(sql, name, binds) cache = {} - if binds.empty? - stmt = @connection.prepare(sql) - else - cache = @statements[sql] ||= { - :stmt => @connection.prepare(sql) - } - stmt = cache[:stmt] - end + log(sql, name, binds) do + if binds.empty? + stmt = @connection.prepare(sql) + else + cache = @statements[sql] ||= { + :stmt => @connection.prepare(sql) + } + stmt = cache[:stmt] + end - begin - stmt.execute(*binds.map { |col, val| type_cast(val, col) }) - rescue Mysql::Error => e - # Older versions of MySQL leave the prepared statement in a bad - # place when an error occurs. To support older mysql versions, we - # need to close the statement and delete the statement from the - # cache. - stmt.close - @statements.delete sql - raise e - end + begin + stmt.execute(*binds.map { |col, val| type_cast(val, col) }) + rescue Mysql::Error => e + # Older versions of MySQL leave the prepared statement in a bad + # place when an error occurs. To support older mysql versions, we + # need to close the statement and delete the statement from the + # cache. + stmt.close + @statements.delete sql + raise e + end - cols = nil - if metadata = stmt.result_metadata - cols = cache[:cols] ||= metadata.fetch_fields.map { |field| - field.name - } - end + cols = nil + if metadata = stmt.result_metadata + cols = cache[:cols] ||= metadata.fetch_fields.map { |field| + field.name + } + end - result = yield [cols, stmt] + result_set = ActiveRecord::Result.new(cols, stmt.to_a) if cols + affected_rows = stmt.affected_rows - stmt.result_metadata.free if cols - stmt.free_result - stmt.close if binds.empty? + stmt.result_metadata.free if cols + stmt.free_result + stmt.close if binds.empty? - result + [result_set, affected_rows] + end end def connect @@ -404,6 +542,13 @@ module ActiveRecord # 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) + + # Make MySQL reject illegal values rather than truncating or + # blanking them. See + # http://dev.mysql.com/doc/refman/5.5/en/server-sql-mode.html#sqlmode_strict_all_tables + if @config.fetch(:strict, true) + execute("SET SQL_MODE='STRICT_ALL_TABLES'", :skip_logging) + end end def select(sql, name = nil, binds = []) diff --git a/activerecord/lib/active_record/connection_adapters/postgresql/array_parser.rb b/activerecord/lib/active_record/connection_adapters/postgresql/array_parser.rb new file mode 100644 index 0000000000..b7d24f2bb3 --- /dev/null +++ b/activerecord/lib/active_record/connection_adapters/postgresql/array_parser.rb @@ -0,0 +1,97 @@ +module ActiveRecord + module ConnectionAdapters + class PostgreSQLColumn < Column + module ArrayParser + private + # Loads pg_array_parser if available. String parsing can be + # performed quicker by a native extension, which will not create + # a large amount of Ruby objects that will need to be garbage + # collected. pg_array_parser has a C and Java extension + begin + require 'pg_array_parser' + include PgArrayParser + rescue LoadError + def parse_pg_array(string) + parse_data(string, 0) + end + end + + def parse_data(string, index) + local_index = index + array = [] + while(local_index < string.length) + case string[local_index] + when '{' + local_index,array = parse_array_contents(array, string, local_index + 1) + when '}' + return array + end + local_index += 1 + end + + array + end + + def parse_array_contents(array, string, index) + is_escaping = false + is_quoted = false + was_quoted = false + current_item = '' + + local_index = index + while local_index + token = string[local_index] + if is_escaping + current_item << token + is_escaping = false + else + if is_quoted + case token + when '"' + is_quoted = false + was_quoted = true + when "\\" + is_escaping = true + else + current_item << token + end + else + case token + when "\\" + is_escaping = true + when ',' + add_item_to_array(array, current_item, was_quoted) + current_item = '' + was_quoted = false + when '"' + is_quoted = true + when '{' + internal_items = [] + local_index,internal_items = parse_array_contents(internal_items, string, local_index + 1) + array.push(internal_items) + when '}' + add_item_to_array(array, current_item, was_quoted) + return local_index,array + else + current_item << token + end + end + end + + local_index += 1 + end + return local_index,array + end + + def add_item_to_array(array, current_item, quoted) + if current_item.length == 0 + elsif !quoted && current_item == 'NULL' + array.push nil + else + array.push current_item + end + end + end + end + end +end diff --git a/activerecord/lib/active_record/connection_adapters/postgresql/cast.rb b/activerecord/lib/active_record/connection_adapters/postgresql/cast.rb new file mode 100644 index 0000000000..62d091357d --- /dev/null +++ b/activerecord/lib/active_record/connection_adapters/postgresql/cast.rb @@ -0,0 +1,124 @@ +module ActiveRecord + module ConnectionAdapters + class PostgreSQLColumn < Column + module Cast + def string_to_time(string) + return string unless String === string + + case string + when 'infinity'; 1.0 / 0.0 + when '-infinity'; -1.0 / 0.0 + else + super + end + end + + def hstore_to_string(object) + if Hash === object + object.map { |k,v| + "#{escape_hstore(k)}=>#{escape_hstore(v)}" + }.join ',' + else + object + end + end + + def string_to_hstore(string) + if string.nil? + nil + elsif String === string + Hash[string.scan(HstorePair).map { |k,v| + v = v.upcase == 'NULL' ? nil : v.gsub(/^"(.*)"$/,'\1').gsub(/\\(.)/, '\1') + k = k.gsub(/^"(.*)"$/,'\1').gsub(/\\(.)/, '\1') + [k,v] + }] + else + string + end + end + + def json_to_string(object) + if Hash === object + ActiveSupport::JSON.encode(object) + else + object + end + end + + def array_to_string(value, column, adapter, should_be_quoted = false) + casted_values = value.map do |val| + if String === val + if val == "NULL" + "\"#{val}\"" + else + quote_and_escape(adapter.type_cast(val, column, true)) + end + else + adapter.type_cast(val, column, true) + end + end + "{#{casted_values.join(',')}}" + end + + def string_to_json(string) + if String === string + ActiveSupport::JSON.decode(string) + else + string + end + end + + def string_to_cidr(string) + if string.nil? + nil + elsif String === string + IPAddr.new(string) + else + string + end + end + + def cidr_to_string(object) + if IPAddr === object + "#{object.to_s}/#{object.instance_variable_get(:@mask_addr).to_s(2).count('1')}" + else + object + end + end + + def string_to_array(string, oid) + parse_pg_array(string).map{|val| oid.type_cast val} + end + + private + + HstorePair = begin + quoted_string = /"[^"\\]*(?:\\.[^"\\]*)*"/ + unquoted_string = /(?:\\.|[^\s,])[^\s=,\\]*(?:\\.[^\s=,\\]*|=[^,>])*/ + /(#{quoted_string}|#{unquoted_string})\s*=>\s*(#{quoted_string}|#{unquoted_string})/ + end + + def escape_hstore(value) + if value.nil? + 'NULL' + else + if value == "" + '""' + else + '"%s"' % value.to_s.gsub(/(["\\])/, '\\\\\1') + end + end + end + + def quote_and_escape(value) + case value + when "NULL" + value + else + "\"#{value.gsub(/"/,"\\\"")}\"" + end + end + end + end + end +end diff --git a/activerecord/lib/active_record/connection_adapters/postgresql/database_statements.rb b/activerecord/lib/active_record/connection_adapters/postgresql/database_statements.rb new file mode 100644 index 0000000000..c8437c18cc --- /dev/null +++ b/activerecord/lib/active_record/connection_adapters/postgresql/database_statements.rb @@ -0,0 +1,240 @@ +require 'active_support/deprecation' + +module ActiveRecord + module ConnectionAdapters + class PostgreSQLAdapter < AbstractAdapter + module DatabaseStatements + def explain(arel, binds = []) + sql = "EXPLAIN #{to_sql(arel, binds)}" + ExplainPrettyPrinter.new.pp(exec_query(sql, 'EXPLAIN', binds)) + end + + class ExplainPrettyPrinter # :nodoc: + # Pretty prints the result of a EXPLAIN in a way that resembles the output of the + # PostgreSQL shell: + # + # QUERY PLAN + # ------------------------------------------------------------------------------ + # Nested Loop Left Join (cost=0.00..37.24 rows=8 width=0) + # Join Filter: (posts.user_id = users.id) + # -> Index Scan using users_pkey on users (cost=0.00..8.27 rows=1 width=4) + # Index Cond: (id = 1) + # -> Seq Scan on posts (cost=0.00..28.88 rows=8 width=4) + # Filter: (posts.user_id = 1) + # (6 rows) + # + def pp(result) + header = result.columns.first + lines = result.rows.map(&:first) + + # We add 2 because there's one char of padding at both sides, note + # the extra hyphens in the example above. + width = [header, *lines].map(&:length).max + 2 + + pp = [] + + pp << header.center(width).rstrip + pp << '-' * width + + pp += lines.map {|line| " #{line}"} + + nrows = result.rows.length + rows_label = nrows == 1 ? 'row' : 'rows' + pp << "(#{nrows} #{rows_label})" + + pp.join("\n") + "\n" + end + end + + # Executes a SELECT query and returns an array of rows. Each row is an + # array of field values. + def select_rows(sql, name = nil) + select_raw(sql, name).last + end + + # Executes an INSERT query and returns the new record's ID + def insert_sql(sql, name = nil, pk = nil, id_value = nil, sequence_name = nil) + unless pk + # Extract the table from the insert sql. Yuck. + table_ref = extract_table_ref_from_insert_sql(sql) + pk = primary_key(table_ref) if table_ref + end + + if pk && use_insert_returning? + select_value("#{sql} RETURNING #{quote_column_name(pk)}") + elsif pk + super + last_insert_id_value(sequence_name || default_sequence_name(table_ref, pk)) + else + super + end + end + + def create + super.insert + end + + # create a 2D array representing the result set + def result_as_array(res) #:nodoc: + # check if we have any binary column and if they need escaping + ftypes = Array.new(res.nfields) do |i| + [i, res.ftype(i)] + end + + rows = res.values + return rows unless ftypes.any? { |_, x| + x == BYTEA_COLUMN_TYPE_OID || x == MONEY_COLUMN_TYPE_OID + } + + typehash = ftypes.group_by { |_, type| type } + binaries = typehash[BYTEA_COLUMN_TYPE_OID] || [] + monies = typehash[MONEY_COLUMN_TYPE_OID] || [] + + rows.each do |row| + # unescape string passed BYTEA field (OID == 17) + binaries.each do |index, _| + row[index] = unescape_bytea(row[index]) + end + + # If this is a money type column and there are any currency symbols, + # then strip them off. Indeed it would be prettier to do this in + # PostgreSQLColumn.string_to_decimal but would break form input + # fields that call value_before_type_cast. + monies.each do |index, _| + data = row[index] + # Because money output is formatted according to the locale, there are two + # cases to consider (note the decimal separators): + # (1) $12,345,678.12 + # (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,]/, '').sub!(/,/, '.') + end + end + end + end + + # Queries the database and returns the results in an Array-like object + def query(sql, name = nil) #:nodoc: + log(sql, name) do + result_as_array @connection.async_exec(sql) + end + end + + # Executes an SQL statement, returning a PGresult object on success + # or raising a PGError exception otherwise. + def execute(sql, name = nil) + log(sql, name) do + @connection.async_exec(sql) + end + end + + def substitute_at(column, index) + Arel::Nodes::BindParam.new "$#{index + 1}" + end + + def exec_query(sql, name = 'SQL', binds = []) + log(sql, name, binds) do + result = binds.empty? ? exec_no_cache(sql, binds) : + exec_cache(sql, binds) + + types = {} + result.fields.each_with_index do |fname, i| + ftype = result.ftype i + fmod = result.fmod i + types[fname] = OID::TYPE_MAP.fetch(ftype, fmod) { |oid, mod| + warn "unknown OID: #{fname}(#{oid}) (#{sql})" + OID::Identity.new + } + end + + ret = ActiveRecord::Result.new(result.fields, result.values, types) + result.clear + return ret + end + end + + def exec_delete(sql, name = 'SQL', binds = []) + log(sql, name, binds) do + result = binds.empty? ? exec_no_cache(sql, binds) : + exec_cache(sql, binds) + affected = result.cmd_tuples + result.clear + affected + end + end + alias :exec_update :exec_delete + + def sql_for_insert(sql, pk, id_value, sequence_name, binds) + unless pk + # Extract the table from the insert sql. Yuck. + table_ref = extract_table_ref_from_insert_sql(sql) + pk = primary_key(table_ref) if table_ref + end + + if pk && use_insert_returning? + sql = "#{sql} RETURNING #{quote_column_name(pk)}" + end + + [sql, binds] + end + + def exec_insert(sql, name, binds, pk = nil, sequence_name = nil) + val = exec_query(sql, name, binds) + if !use_insert_returning? && pk + unless sequence_name + table_ref = extract_table_ref_from_insert_sql(sql) + sequence_name = default_sequence_name(table_ref, pk) + return val unless sequence_name + end + last_insert_id_result(sequence_name) + else + val + end + end + + # Executes an UPDATE query and returns the number of affected tuples. + def update_sql(sql, name = nil) + super.cmd_tuples + end + + # Begins a transaction. + def begin_db_transaction + execute "BEGIN" + end + + # Commits a transaction. + def commit_db_transaction + execute "COMMIT" + end + + # Aborts a transaction. + def rollback_db_transaction + execute "ROLLBACK" + end + + def outside_transaction? + ActiveSupport::Deprecation.warn( + "#outside_transaction? is deprecated. This method was only really used " \ + "internally, but you can use #transaction_open? instead." + ) + @connection.transaction_status == PGconn::PQTRANS_IDLE + 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 + end + end + end +end diff --git a/activerecord/lib/active_record/connection_adapters/postgresql/oid.rb b/activerecord/lib/active_record/connection_adapters/postgresql/oid.rb index c82afc232c..52344f61c0 100644 --- a/activerecord/lib/active_record/connection_adapters/postgresql/oid.rb +++ b/activerecord/lib/active_record/connection_adapters/postgresql/oid.rb @@ -63,6 +63,21 @@ module ActiveRecord end end + class Array < Type + attr_reader :subtype + def initialize(subtype) + @subtype = subtype + end + + def type_cast(value) + if String === value + ConnectionAdapters::PostgreSQLColumn.string_to_array value, @subtype + else + value + end + end + end + class Integer < Type def type_cast(value) return if value.nil? @@ -137,6 +152,22 @@ module ActiveRecord end end + class Cidr < Type + def type_cast(value) + return if value.nil? + + ConnectionAdapters::PostgreSQLColumn.string_to_cidr value + end + end + + class Json < Type + def type_cast(value) + return if value.nil? + + ConnectionAdapters::PostgreSQLColumn.string_to_json value + end + end + class TypeMap def initialize @mapping = {} @@ -212,11 +243,10 @@ module ActiveRecord # FIXME: why are we keeping these types as strings? alias_type 'tsvector', 'text' alias_type 'interval', 'text' - alias_type 'cidr', 'text' - alias_type 'inet', 'text' - alias_type 'macaddr', 'text' alias_type 'bit', 'text' alias_type 'varbit', 'text' + alias_type 'macaddr', 'text' + alias_type 'uuid', 'text' # FIXME: I don't think this is correct. We should probably be returning a parsed date, # but the tests pass with a string returned. @@ -237,6 +267,10 @@ module ActiveRecord register_type 'polygon', OID::Identity.new register_type 'circle', OID::Identity.new register_type 'hstore', OID::Hstore.new + register_type 'json', OID::Json.new + + register_type 'cidr', OID::Cidr.new + alias_type 'inet', 'cidr' end end end diff --git a/activerecord/lib/active_record/connection_adapters/postgresql/quoting.rb b/activerecord/lib/active_record/connection_adapters/postgresql/quoting.rb new file mode 100644 index 0000000000..37d43d891d --- /dev/null +++ b/activerecord/lib/active_record/connection_adapters/postgresql/quoting.rb @@ -0,0 +1,141 @@ +module ActiveRecord + module ConnectionAdapters + class PostgreSQLAdapter < AbstractAdapter + module Quoting + # Escapes binary strings for bytea input to the database. + def escape_bytea(value) + PGconn.escape_bytea(value) if value + end + + # Unescapes bytea output from a database to the binary string it represents. + # NOTE: This is NOT an inverse of escape_bytea! This is only to be used + # on escaped binary output from database drive. + def unescape_bytea(value) + PGconn.unescape_bytea(value) if value + end + + # Quotes PostgreSQL-specific data types for SQL input. + def quote(value, column = nil) #:nodoc: + return super unless column + + case value + when Array + if column.array + "'#{PostgreSQLColumn.array_to_string(value, column, self)}'" + else + super + end + when Hash + case column.sql_type + when 'hstore' then super(PostgreSQLColumn.hstore_to_string(value), column) + when 'json' then super(PostgreSQLColumn.json_to_string(value), column) + else super + end + when IPAddr + case column.sql_type + when 'inet', 'cidr' then super(PostgreSQLColumn.cidr_to_string(value), column) + else super + end + when Float + if value.infinite? && column.type == :datetime + "'#{value.to_s.downcase}'" + elsif value.infinite? || value.nan? + "'#{value.to_s}'" + else + super + end + when Numeric + return super unless column.sql_type == 'money' + # Not truly string input, so doesn't require (or allow) escape string syntax. + "'#{value}'" + 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 + end + end + + def type_cast(value, column, array_member = false) + return super(value, column) unless column + + case value + when NilClass + if column.array && array_member + 'NULL' + elsif column.array + value + else + super(value, column) + end + when Array + return super(value, column) unless column.array + PostgreSQLColumn.array_to_string(value, column, self) + when String + return super(value, column) unless 'bytea' == column.sql_type + { :value => value, :format => 1 } + when Hash + case column.sql_type + when 'hstore' then PostgreSQLColumn.hstore_to_string(value) + when 'json' then PostgreSQLColumn.json_to_string(value) + else super(value, column) + end + when IPAddr + return super(value, column) unless ['inet','cidr'].includes? column.sql_type + PostgreSQLColumn.cidr_to_string(value) + else + super(value, column) + end + end + + # Quotes strings for use in SQL input. + def quote_string(s) #:nodoc: + @connection.escape(s) + end + + # Checks the following cases: + # + # - table_name + # - "table.name" + # - schema_name.table_name + # - schema_name."table.name" + # - "schema.name".table_name + # - "schema.name"."table.name" + def quote_table_name(name) + schema, name_part = extract_pg_identifier_from_name(name.to_s) + + unless name_part + quote_column_name(schema) + else + table_name, name_part = extract_pg_identifier_from_name(name_part) + "#{quote_column_name(schema)}.#{quote_column_name(table_name)}" + end + end + + # Quotes column names for use in SQL queries. + def quote_column_name(name) #:nodoc: + PGconn.quote_ident(name.to_s) + end + + # 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) + "#{super}.#{sprintf("%06d", value.usec)}" + else + super + end + end + end + end + end +end diff --git a/activerecord/lib/active_record/connection_adapters/postgresql/referential_integrity.rb b/activerecord/lib/active_record/connection_adapters/postgresql/referential_integrity.rb new file mode 100644 index 0000000000..16da3ea732 --- /dev/null +++ b/activerecord/lib/active_record/connection_adapters/postgresql/referential_integrity.rb @@ -0,0 +1,22 @@ +module ActiveRecord + module ConnectionAdapters + class PostgreSQLAdapter < AbstractAdapter + module ReferentialIntegrity + def supports_disable_referential_integrity? #:nodoc: + true + end + + def disable_referential_integrity #:nodoc: + if supports_disable_referential_integrity? then + execute(tables.collect { |name| "ALTER TABLE #{quote_table_name(name)} DISABLE TRIGGER ALL" }.join(";")) + end + yield + ensure + if supports_disable_referential_integrity? then + execute(tables.collect { |name| "ALTER TABLE #{quote_table_name(name)} ENABLE TRIGGER ALL" }.join(";")) + end + end + end + end + end +end diff --git a/activerecord/lib/active_record/connection_adapters/postgresql/schema_statements.rb b/activerecord/lib/active_record/connection_adapters/postgresql/schema_statements.rb new file mode 100644 index 0000000000..8a073bf878 --- /dev/null +++ b/activerecord/lib/active_record/connection_adapters/postgresql/schema_statements.rb @@ -0,0 +1,446 @@ +module ActiveRecord + module ConnectionAdapters + class PostgreSQLAdapter < AbstractAdapter + module SchemaStatements + # Drops the database specified on the +name+ attribute + # and creates it again using the provided +options+. + def recreate_database(name, options = {}) #:nodoc: + drop_database(name) + create_database(name, options) + end + + # Create a new PostgreSQL database. Options include <tt>:owner</tt>, <tt>:template</tt>, + # <tt>:encoding</tt>, <tt>:collation</tt>, <tt>:ctype</tt>, + # <tt>:tablespace</tt>, and <tt>:connection_limit</tt> (note that MySQL uses + # <tt>:charset</tt> while PostgreSQL uses <tt>:encoding</tt>). + # + # Example: + # create_database config[:database], config + # create_database 'foo_development', :encoding => 'unicode' + def create_database(name, options = {}) + options = options.reverse_merge(:encoding => "utf8") + + option_string = options.symbolize_keys.sum do |key, value| + case key + when :owner + " OWNER = \"#{value}\"" + when :template + " TEMPLATE = \"#{value}\"" + when :encoding + " ENCODING = '#{value}'" + when :collation + " LC_COLLATE = '#{value}'" + when :ctype + " LC_CTYPE = '#{value}'" + when :tablespace + " TABLESPACE = \"#{value}\"" + when :connection_limit + " CONNECTION LIMIT = #{value}" + else + "" + end + end + + execute "CREATE DATABASE #{quote_table_name(name)}#{option_string}" + end + + # Drops a PostgreSQL database. + # + # Example: + # drop_database 'matt_development' + def drop_database(name) #:nodoc: + execute "DROP DATABASE IF EXISTS #{quote_table_name(name)}" + end + + # Returns the list of all tables in the schema search path or a specified schema. + def tables(name = nil) + query(<<-SQL, 'SCHEMA').map { |row| row[0] } + SELECT tablename + FROM pg_tables + WHERE schemaname = ANY (current_schemas(false)) + SQL + end + + # Returns true if table exists. + # If the schema is not specified as part of +name+ then it will only find tables within + # the current schema search path (regardless of permissions to access tables in other schemas) + def table_exists?(name) + schema, table = Utils.extract_schema_and_table(name.to_s) + return false unless table + + binds = [[nil, table]] + binds << [nil, schema] if schema + + exec_query(<<-SQL, 'SCHEMA').rows.first[0].to_i > 0 + SELECT COUNT(*) + FROM pg_class c + LEFT JOIN pg_namespace n ON n.oid = c.relnamespace + WHERE c.relkind in ('v','r') + AND c.relname = '#{table.gsub(/(^"|"$)/,'')}' + AND n.nspname = #{schema ? "'#{schema}'" : 'ANY (current_schemas(false))'} + SQL + end + + # Returns true if schema exists. + def schema_exists?(name) + exec_query(<<-SQL, 'SCHEMA').rows.first[0].to_i > 0 + SELECT COUNT(*) + FROM pg_namespace + WHERE nspname = '#{name}' + SQL + end + + # Returns an array of indexes for the given table. + def indexes(table_name, name = nil) + result = query(<<-SQL, 'SCHEMA') + SELECT distinct i.relname, d.indisunique, d.indkey, pg_get_indexdef(d.indexrelid), t.oid + FROM pg_class t + INNER JOIN pg_index d ON t.oid = d.indrelid + INNER JOIN pg_class i ON d.indexrelid = i.oid + WHERE i.relkind = 'i' + AND d.indisprimary = 'f' + AND t.relname = '#{table_name}' + AND i.relnamespace IN (SELECT oid FROM pg_namespace WHERE nspname = ANY (current_schemas(false)) ) + ORDER BY i.relname + SQL + + result.map do |row| + index_name = row[0] + unique = row[1] == 't' + indkey = row[2].split(" ") + inddef = row[3] + oid = row[4] + + columns = Hash[query(<<-SQL, "SCHEMA")] + SELECT a.attnum, a.attname + FROM pg_attribute a + WHERE a.attrelid = #{oid} + AND a.attnum IN (#{indkey.join(",")}) + SQL + + column_names = columns.values_at(*indkey).compact + + # add info on sort order for columns (only desc order is explicitly specified, asc is the default) + desc_order_columns = inddef.scan(/(\w+) DESC/).flatten + orders = desc_order_columns.any? ? Hash[desc_order_columns.map {|order_column| [order_column, :desc]}] : {} + where = inddef.scan(/WHERE (.+)$/).flatten[0] + + column_names.empty? ? nil : IndexDefinition.new(table_name, index_name, unique, column_names, [], orders, where) + end.compact + end + + # Returns the list of all column definitions for a table. + def columns(table_name) + # Limit, precision, and scale are all handled by the superclass. + column_definitions(table_name).map do |column_name, type, default, notnull, oid, fmod| + oid = OID::TYPE_MAP.fetch(oid.to_i, fmod.to_i) { + OID::Identity.new + } + PostgreSQLColumn.new(column_name, default, oid, type, notnull == 'f') + end + end + + # Returns the current database name. + def current_database + query('select current_database()', 'SCHEMA')[0][0] + end + + # Returns the current schema name. + def current_schema + query('SELECT current_schema', 'SCHEMA')[0][0] + end + + # Returns the current database encoding format. + def encoding + query(<<-end_sql, 'SCHEMA')[0][0] + SELECT pg_encoding_to_char(pg_database.encoding) FROM pg_database + WHERE pg_database.datname LIKE '#{current_database}' + end_sql + end + + # Returns the current database collation. + def collation + query(<<-end_sql, 'SCHEMA')[0][0] + SELECT pg_database.datcollate FROM pg_database WHERE pg_database.datname LIKE '#{current_database}' + end_sql + end + + # Returns the current database ctype. + def ctype + query(<<-end_sql, 'SCHEMA')[0][0] + SELECT pg_database.datctype FROM pg_database WHERE pg_database.datname LIKE '#{current_database}' + end_sql + end + + # Returns an array of schema names. + def schema_names + query(<<-SQL, 'SCHEMA').flatten + SELECT nspname + FROM pg_namespace + WHERE nspname !~ '^pg_.*' + AND nspname NOT IN ('information_schema') + ORDER by nspname; + SQL + end + + # Creates a schema for the given schema name. + def create_schema schema_name + execute "CREATE SCHEMA #{schema_name}" + end + + # Drops the schema for the given schema name. + def drop_schema schema_name + execute "DROP SCHEMA #{schema_name} CASCADE" + end + + # Sets the schema search path to a string of comma-separated schema names. + # Names beginning with $ have to be quoted (e.g. $user => '$user'). + # See: http://www.postgresql.org/docs/current/static/ddl-schemas.html + # + # This should be not be called manually but set in database.yml. + def schema_search_path=(schema_csv) + if schema_csv + execute("SET search_path TO #{schema_csv}", 'SCHEMA') + @schema_search_path = schema_csv + end + end + + # Returns the active schema search path. + def schema_search_path + @schema_search_path ||= query('SHOW search_path', 'SCHEMA')[0][0] + end + + # Returns the current client message level. + def client_min_messages + query('SHOW client_min_messages', 'SCHEMA')[0][0] + end + + # Set the client message level. + def client_min_messages=(level) + execute("SET client_min_messages TO '#{level}'", 'SCHEMA') + end + + # Returns the sequence name for a table's primary key or some other specified key. + def default_sequence_name(table_name, pk = nil) #:nodoc: + result = serial_sequence(table_name, pk || 'id') + return nil unless result + result.split('.').last + rescue ActiveRecord::StatementInvalid + "#{table_name}_#{pk || 'id'}_seq" + end + + def serial_sequence(table, column) + result = exec_query(<<-eosql, 'SCHEMA') + SELECT pg_get_serial_sequence('#{table}', '#{column}') + eosql + result.rows.first.first + end + + # Resets the sequence of a table's primary key to the maximum value. + def reset_pk_sequence!(table, pk = nil, sequence = nil) #:nodoc: + unless pk and sequence + default_pk, default_sequence = pk_and_sequence_for(table) + + pk ||= default_pk + sequence ||= default_sequence + end + + if @logger && pk && !sequence + @logger.warn "#{table} has primary key #{pk} with no default sequence" + end + + if pk && sequence + quoted_sequence = quote_table_name(sequence) + + select_value <<-end_sql, 'SCHEMA' + SELECT setval('#{quoted_sequence}', (SELECT COALESCE(MAX(#{quote_column_name pk})+(SELECT increment_by FROM #{quoted_sequence}), (SELECT min_value FROM #{quoted_sequence})) FROM #{quote_table_name(table)}), false) + end_sql + end + end + + # Returns a table's primary key and belonging sequence. + 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, 'SCHEMA')[0] + SELECT attr.attname, seq.relname + FROM pg_class seq, + pg_attribute attr, + pg_depend dep, + pg_namespace name, + pg_constraint cons + WHERE seq.oid = dep.objid + AND seq.relkind = 'S' + AND attr.attrelid = dep.refobjid + AND attr.attnum = dep.refobjsubid + AND attr.attrelid = cons.conrelid + AND attr.attnum = cons.conkey[1] + AND cons.contype = 'p' + AND dep.refobjid = '#{quote_table_name(table)}'::regclass + end_sql + + if result.nil? or result.empty? + # If that fails, try parsing the primary key's default value. + # Support the 7.x and 8.0 nextval('foo'::text) as well as + # the 8.1+ nextval('foo'::regclass). + result = query(<<-end_sql, 'SCHEMA')[0] + SELECT attr.attname, + CASE + WHEN split_part(def.adsrc, '''', 2) ~ '.' THEN + substr(split_part(def.adsrc, '''', 2), + strpos(split_part(def.adsrc, '''', 2), '.')+1) + ELSE split_part(def.adsrc, '''', 2) + END + FROM pg_class t + JOIN pg_attribute attr ON (t.oid = attrelid) + JOIN pg_attrdef def ON (adrelid = attrelid AND adnum = attnum) + JOIN pg_constraint cons ON (conrelid = adrelid AND adnum = conkey[1]) + WHERE t.oid = '#{quote_table_name(table)}'::regclass + AND cons.contype = 'p' + AND def.adsrc ~* 'nextval' + end_sql + end + + [result.first, result.last] + rescue + nil + end + + # Returns just a table's primary key + def primary_key(table) + row = exec_query(<<-end_sql, 'SCHEMA').rows.first + SELECT DISTINCT(attr.attname) + FROM pg_attribute attr + INNER JOIN pg_depend dep ON attr.attrelid = dep.refobjid AND attr.attnum = dep.refobjsubid + INNER JOIN pg_constraint cons ON attr.attrelid = cons.conrelid AND attr.attnum = cons.conkey[1] + WHERE cons.contype = 'p' + AND dep.refobjid = '#{table}'::regclass + end_sql + + row && row.first + end + + # Renames a table. + # Also renames a table's primary key sequence if the sequence name matches the + # Active Record default. + # + # Example: + # rename_table('octopuses', 'octopi') + def rename_table(name, new_name) + clear_cache! + execute "ALTER TABLE #{quote_table_name(name)} RENAME TO #{quote_table_name(new_name)}" + pk, seq = pk_and_sequence_for(new_name) + if seq == "#{name}_#{pk}_seq" + new_seq = "#{new_name}_#{pk}_seq" + execute "ALTER TABLE #{quote_table_name(seq)} RENAME TO #{quote_table_name(new_seq)}" + end + end + + # 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 = {}) + clear_cache! + 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) + + execute add_column_sql + end + + # Changes the column of a table. + def change_column(table_name, column_name, type, options = {}) + clear_cache! + quoted_table_name = quote_table_name(table_name) + + execute "ALTER TABLE #{quoted_table_name} ALTER COLUMN #{quote_column_name(column_name)} TYPE #{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 + + # Changes the default value of a table column. + def change_column_default(table_name, column_name, default) + clear_cache! + execute "ALTER TABLE #{quote_table_name(table_name)} ALTER COLUMN #{quote_column_name(column_name)} SET DEFAULT #{quote(default)}" + end + + def change_column_null(table_name, column_name, null, default = nil) + clear_cache! + unless null || default.nil? + execute("UPDATE #{quote_table_name(table_name)} SET #{quote_column_name(column_name)}=#{quote(default)} WHERE #{quote_column_name(column_name)} IS NULL") + end + execute("ALTER TABLE #{quote_table_name(table_name)} ALTER #{quote_column_name(column_name)} #{null ? 'DROP' : 'SET'} NOT NULL") + end + + # Renames a column in a table. + def rename_column(table_name, column_name, new_column_name) + clear_cache! + execute "ALTER TABLE #{quote_table_name(table_name)} RENAME COLUMN #{quote_column_name(column_name)} TO #{quote_column_name(new_column_name)}" + end + + def remove_index!(table_name, index_name) #:nodoc: + 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 + + # Maps logical Rails types to PostgreSQL-specific data types. + def type_to_sql(type, limit = nil, precision = nil, scale = nil) + case type.to_s + when 'binary' + # PostgreSQL doesn't support limits on binary (bytea) columns. + # The hard limit is 1Gb, because of a 32-bit size field, and TOAST. + case limit + when nil, 0..0x3fffffff; super(type) + else raise(ActiveRecordError, "No binary type has byte size #{limit}.") + end + when 'integer' + return 'integer' unless limit + + case limit + when 1, 2; 'smallint' + when 3, 4; 'integer' + when 5..8; 'bigint' + else raise(ActiveRecordError, "No integer type has byte size #{limit}. Use a numeric with precision 0 instead.") + end + when 'datetime' + return super unless precision + + case precision + when 0..6; "timestamp(#{precision})" + else raise(ActiveRecordError, "No timestamp type has precision of #{precision}. The allowed range of precision is from 0 to 6") + end + else + super + end + end + + # Returns a SELECT DISTINCT clause for a given set of columns and a given ORDER BY clause. + # + # PostgreSQL requires the ORDER BY columns in the select list for distinct queries, and + # requires that the ORDER BY include the distinct column. + # + # distinct("posts.id", "posts.created_at desc") + 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 = orders.collect do |s| + s = s.to_sql unless s.is_a?(String) + s.gsub(/\s+(ASC|DESC)\s*(NULLS\s+(FIRST|LAST)\s*)?/i, '') + end + 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}" } + + "DISTINCT #{columns}, #{order_columns * ', '}" + end + end + end + end +end diff --git a/activerecord/lib/active_record/connection_adapters/postgresql_adapter.rb b/activerecord/lib/active_record/connection_adapters/postgresql_adapter.rb index c675b64a26..761052a788 100644 --- a/activerecord/lib/active_record/connection_adapters/postgresql_adapter.rb +++ b/activerecord/lib/active_record/connection_adapters/postgresql_adapter.rb @@ -1,13 +1,20 @@ require 'active_record/connection_adapters/abstract_adapter' -require 'active_support/core_ext/object/blank' require 'active_record/connection_adapters/statement_pool' require 'active_record/connection_adapters/postgresql/oid' +require 'active_record/connection_adapters/postgresql/cast' +require 'active_record/connection_adapters/postgresql/array_parser' +require 'active_record/connection_adapters/postgresql/quoting' +require 'active_record/connection_adapters/postgresql/schema_statements' +require 'active_record/connection_adapters/postgresql/database_statements' +require 'active_record/connection_adapters/postgresql/referential_integrity' require 'arel/visitors/bind_visitor' # Make sure we're using pg high enough for PGResult#values gem 'pg', '~> 0.11' require 'pg' +require 'ipaddr' + module ActiveRecord module ConnectionHandling # Establishes a connection to the database that's used by all Active Record objects @@ -16,8 +23,8 @@ module ActiveRecord # Forward any unused config params to PGconn.connect. [:statement_limit, :encoding, :min_messages, :schema_search_path, - :schema_order, :adapter, :pool, :wait_timeout, :template, - :reaping_frequency].each do |key| + :schema_order, :adapter, :pool, :checkout_timeout, :template, + :reaping_frequency, :insert_returning].each do |key| conn_params.delete key end conn_params.delete_if { |k,v| v.nil? } @@ -35,63 +42,24 @@ module ActiveRecord module ConnectionAdapters # PostgreSQL-specific extensions to column definitions in a table. class PostgreSQLColumn < Column #:nodoc: + attr_accessor :array # Instantiates a new PostgreSQL column definition in a table. def initialize(name, default, oid_type, sql_type = nil, null = true) @oid_type = oid_type - super(name, self.class.extract_value_from_default(default), sql_type, null) + if sql_type =~ /\[\]$/ + @array = true + super(name, self.class.extract_value_from_default(default), sql_type[0..sql_type.length - 3], null) + else + @array = false + super(name, self.class.extract_value_from_default(default), sql_type, null) + end end # :stopdoc: class << self + include ConnectionAdapters::PostgreSQLColumn::Cast + include ConnectionAdapters::PostgreSQLColumn::ArrayParser attr_accessor :money_precision - def string_to_time(string) - return string unless String === string - - case string - when 'infinity' then 1.0 / 0.0 - when '-infinity' then -1.0 / 0.0 - else - super - end - end - - def hstore_to_string(object) - if Hash === object - object.map { |k,v| - "#{escape_hstore(k)}=>#{escape_hstore(v)}" - }.join ',' - else - object - end - end - - def string_to_hstore(string) - if string.nil? - nil - elsif String === string - Hash[string.scan(HstorePair).map { |k,v| - v = v.upcase == 'NULL' ? nil : v.gsub(/^"(.*)"$/,'\1').gsub(/\\(.)/, '\1') - k = k.gsub(/^"(.*)"$/,'\1').gsub(/\\(.)/, '\1') - [k,v] - }] - else - string - end - end - - private - HstorePair = begin - quoted_string = /"[^"\\]*(?:\\.[^"\\]*)*"/ - unquoted_string = /(?:\\.|[^\s,])[^\s=,\\]*(?:\\.[^\s=,\\]*|=[^,>])*/ - /(#{quoted_string}|#{unquoted_string})\s*=>\s*(#{quoted_string}|#{unquoted_string})/ - end - - def escape_hstore(value) - value.nil? ? 'NULL' - : value =~ /[=\s,>]/ ? '"%s"' % value.gsub(/(["\\])/, '\\\\\1') - : value == "" ? '""' - : value.to_s.gsub(/(["\\])/, '\\\\\1') - end end # :startdoc: @@ -146,6 +114,9 @@ module ActiveRecord # Hstore when /\A'(.*)'::hstore\z/ $1 + # JSON + when /\A'(.*)'::json\z/ + $1 # Object identifier types when /\A-?\d+\z/ $1 @@ -164,83 +135,94 @@ module ActiveRecord end private - def extract_limit(sql_type) - case sql_type - when /^bigint/i; 8 - when /^smallint/i; 2 - else super + + def extract_limit(sql_type) + case sql_type + when /^bigint/i; 8 + when /^smallint/i; 2 + when /^timestamp/i; nil + else super + end end - end - # Extracts the scale from PostgreSQL-specific data types. - def extract_scale(sql_type) - # Money type has a fixed scale of 2. - sql_type =~ /^money/ ? 2 : super - end + # Extracts the scale from PostgreSQL-specific data types. + def extract_scale(sql_type) + # Money type has a fixed scale of 2. + sql_type =~ /^money/ ? 2 : super + end - # Extracts the precision from PostgreSQL-specific data types. - def extract_precision(sql_type) - if sql_type == 'money' - self.class.money_precision - else - super + # Extracts the precision from PostgreSQL-specific data types. + def extract_precision(sql_type) + if sql_type == 'money' + self.class.money_precision + elsif sql_type =~ /timestamp/i + $1.to_i if sql_type =~ /\((\d+)\)/ + else + super + end end - end - # Maps PostgreSQL-specific data types to logical Rails types. - def simplified_type(field_type) - case field_type - # Numeric and monetary types - when /^(?:real|double precision)$/ - :float - # Monetary types - when 'money' - :decimal - when 'hstore' - :hstore - # Character types - when /^(?:character varying|bpchar)(?:\(\d+\))?$/ - :string - # Binary data types - when 'bytea' - :binary - # Date/time types - when /^timestamp with(?:out)? time zone$/ - :datetime - when 'interval' - :string - # Geometric types - when /^(?:point|line|lseg|box|"?path"?|polygon|circle)$/ - :string - # Network address types - when /^(?:cidr|inet|macaddr)$/ - :string - # Bit strings - when /^bit(?: varying)?(?:\(\d+\))?$/ - :string - # XML type - when 'xml' - :xml - # tsvector type - when 'tsvector' - :tsvector - # Arrays - when /^\D+\[\]$/ - :string - # Object identifier types - when 'oid' - :integer - # UUID type - when 'uuid' - :string - # Small and big integer types - when /^(?:small|big)int$/ - :integer - # Pass through all types that are not specific to PostgreSQL. - else - super + # Maps PostgreSQL-specific data types to logical Rails types. + def simplified_type(field_type) + case field_type + # Numeric and monetary types + when /^(?:real|double precision)$/ + :float + # Monetary types + when 'money' + :decimal + when 'hstore' + :hstore + # Network address types + when 'inet' + :inet + when 'cidr' + :cidr + when 'macaddr' + :macaddr + # Character types + when /^(?:character varying|bpchar)(?:\(\d+\))?$/ + :string + # Binary data types + when 'bytea' + :binary + # Date/time types + when /^timestamp with(?:out)? time zone$/ + :datetime + when /^interval(?:|\(\d+\))$/ + :string + # Geometric types + when /^(?:point|line|lseg|box|"?path"?|polygon|circle)$/ + :string + # Bit strings + when /^bit(?: varying)?(?:\(\d+\))?$/ + :string + # XML type + when 'xml' + :xml + # tsvector type + when 'tsvector' + :tsvector + # Arrays + when /^\D+\[\]$/ + :string + # Object identifier types + when 'oid' + :integer + # UUID type + when 'uuid' + :uuid + # JSON type + when 'json' + :json + # Small and big integer types + when /^(?:small|big)int$/ + :integer + # Pass through all types that are not specific to PostgreSQL. + else + super + end end - end end # The PostgreSQL adapter works with the native C (https://bitbucket.org/ged/ruby-pg) driver. @@ -259,6 +241,8 @@ module ActiveRecord # <encoding></tt> call on the connection. # * <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>:insert_returning</tt> - An optional boolean to control the use or <tt>RETURNING</tt> for <tt>INSERT<tt> statements + # defaults to true. # # Any further options are used as connection parameters to libpq. See # http://www.postgresql.org/docs/9.1/static/libpq-connect.html for the @@ -267,6 +251,10 @@ module ActiveRecord # In addition, default connection parameters of libpq can be set per environment variables. # See http://www.postgresql.org/docs/9.1/static/libpq-envars.html . class PostgreSQLAdapter < AbstractAdapter + class ColumnDefinition < ActiveRecord::ConnectionAdapters::ColumnDefinition + attr_accessor :array + end + class TableDefinition < ActiveRecord::ConnectionAdapters::TableDefinition def xml(*args) options = args.extract_options! @@ -281,33 +269,93 @@ module ActiveRecord def hstore(name, options = {}) column(name, 'hstore', options) end + + def inet(name, options = {}) + column(name, 'inet', options) + end + + def cidr(name, options = {}) + column(name, 'cidr', options) + end + + def macaddr(name, options = {}) + column(name, 'macaddr', options) + end + + def uuid(name, options = {}) + column(name, 'uuid', options) + end + + def json(name, options = {}) + column(name, 'json', options) + end + + def column(name, type = nil, options = {}) + super + column = self[name] + column.array = options[:array] + + self + end + + private + + def new_column_definition(base, name, type) + definition = ColumnDefinition.new base, name, type + @columns << definition + @columns_hash[name] = definition + definition + end end ADAPTER_NAME = 'PostgreSQL' NATIVE_DATABASE_TYPES = { - :primary_key => "serial primary key", - :string => { :name => "character varying", :limit => 255 }, - :text => { :name => "text" }, - :integer => { :name => "integer" }, - :float => { :name => "float" }, - :decimal => { :name => "decimal" }, - :datetime => { :name => "timestamp" }, - :timestamp => { :name => "timestamp" }, - :time => { :name => "time" }, - :date => { :name => "date" }, - :binary => { :name => "bytea" }, - :boolean => { :name => "boolean" }, - :xml => { :name => "xml" }, - :tsvector => { :name => "tsvector" }, - :hstore => { :name => "hstore" } + primary_key: "serial primary key", + string: { name: "character varying", limit: 255 }, + text: { name: "text" }, + integer: { name: "integer" }, + float: { name: "float" }, + decimal: { name: "decimal" }, + datetime: { name: "timestamp" }, + timestamp: { name: "timestamp" }, + time: { name: "time" }, + date: { name: "date" }, + binary: { name: "bytea" }, + boolean: { name: "boolean" }, + xml: { name: "xml" }, + tsvector: { name: "tsvector" }, + hstore: { name: "hstore" }, + inet: { name: "inet" }, + cidr: { name: "cidr" }, + macaddr: { name: "macaddr" }, + uuid: { name: "uuid" }, + json: { name: "json" } } + include Quoting + include ReferentialIntegrity + include SchemaStatements + include DatabaseStatements + # Returns 'PostgreSQL' as adapter name for identification purposes. def adapter_name ADAPTER_NAME end + # Adds `:array` option to the default set provided by the + # AbstractAdapter + def prepare_column_options(column, types) + spec = super + spec[:array] = 'true' if column.respond_to?(:array) && column.array + spec + end + + # Adds `:array` as a valid migration key + def migration_keys + super + [:array] + end + # Returns +true+, since this connection adapter supports prepared statement # caching. def supports_statement_cache? @@ -359,19 +407,20 @@ module ActiveRecord end private - def cache - @cache[Process.pid] - end - def dealloc(key) - @connection.query "DEALLOCATE #{key}" if connection_active? - end + def cache + @cache[Process.pid] + end - def connection_active? - @connection.status == PGconn::CONNECTION_OK - rescue PGError - false - end + def dealloc(key) + @connection.query "DEALLOCATE #{key}" if connection_active? + end + + def connection_active? + @connection.status == PGconn::CONNECTION_OK + rescue PGError + false + end end class BindSubstitution < Arel::Visitors::PostgreSQL # :nodoc: @@ -406,6 +455,7 @@ module ActiveRecord initialize_type_map @local_tz = execute('SHOW TIME ZONE', 'SCHEMA').first["TimeZone"] + @use_insert_returning = @config.key?(:insert_returning) ? @config[:insert_returning] : true end # Clears the prepared statements cache. @@ -415,14 +465,15 @@ module ActiveRecord # Is this connection alive and ready for queries? def active? - @connection.status == PGconn::CONNECTION_OK + @connection.query 'SELECT 1' + true rescue PGError false end # Close then reopen the connection. def reconnect! - clear_cache! + super @connection.reset configure_connection end @@ -435,7 +486,7 @@ module ActiveRecord # Disconnects from the database if already connected. Otherwise, this # method does nothing. def disconnect! - clear_cache! + super @connection.close rescue nil end @@ -481,109 +532,14 @@ module ActiveRecord # Returns the configured supported identifier length supported by PostgreSQL def table_alias_length - @table_alias_length ||= query('SHOW max_identifier_length')[0][0].to_i - end - - # QUOTING ================================================== - - # Escapes binary strings for bytea input to the database. - def escape_bytea(value) - PGconn.escape_bytea(value) if value - end - - # Unescapes bytea output from a database to the binary string it represents. - # NOTE: This is NOT an inverse of escape_bytea! This is only to be used - # on escaped binary output from database drive. - def unescape_bytea(value) - PGconn.unescape_bytea(value) if value - end - - # Quotes PostgreSQL-specific data types for SQL input. - def quote(value, column = nil) #:nodoc: - return super unless column - - case value - when Hash - case column.sql_type - when 'hstore' then super(PostgreSQLColumn.hstore_to_string(value), column) - else super - end - when Float - return super unless value.infinite? && column.type == :datetime - "'#{value.to_s.downcase}'" - when Numeric - return super unless column.sql_type == 'money' - # Not truly string input, so doesn't require (or allow) escape string syntax. - "'#{value}'" - 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 - end - end - - def type_cast(value, column) - return super unless column - - case value - when String - return super unless 'bytea' == column.sql_type - { :value => value, :format => 1 } - when Hash - return super unless 'hstore' == column.sql_type - PostgreSQLColumn.hstore_to_string(value) - else - super - end - end - - # Quotes strings for use in SQL input. - def quote_string(s) #:nodoc: - @connection.escape(s) - end - - # Checks the following cases: - # - # - table_name - # - "table.name" - # - schema_name.table_name - # - schema_name."table.name" - # - "schema.name".table_name - # - "schema.name"."table.name" - def quote_table_name(name) - schema, name_part = extract_pg_identifier_from_name(name.to_s) - - unless name_part - quote_column_name(schema) - else - table_name, name_part = extract_pg_identifier_from_name(name_part) - "#{quote_column_name(schema)}.#{quote_column_name(table_name)}" - end - end - - # Quotes column names for use in SQL queries. - def quote_column_name(name) #:nodoc: - PGconn.quote_ident(name.to_s) + @table_alias_length ||= query('SHOW max_identifier_length', 'SCHEMA')[0][0].to_i end - # 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) - "#{super}.#{sprintf("%06d", value.usec)}" - else - super + def add_column_options!(sql, options) + if options[:array] || options[:column].try(:array) + sql << '[]' end + super end # Set the authorized user for this session @@ -592,610 +548,6 @@ module ActiveRecord exec_query "SET SESSION AUTHORIZATION #{user}" end - # REFERENTIAL INTEGRITY ==================================== - - def supports_disable_referential_integrity? #:nodoc: - true - end - - def disable_referential_integrity #:nodoc: - if supports_disable_referential_integrity? then - execute(tables.collect { |name| "ALTER TABLE #{quote_table_name(name)} DISABLE TRIGGER ALL" }.join(";")) - end - yield - ensure - if supports_disable_referential_integrity? then - execute(tables.collect { |name| "ALTER TABLE #{quote_table_name(name)} ENABLE TRIGGER ALL" }.join(";")) - end - end - - # DATABASE STATEMENTS ====================================== - - def explain(arel, binds = []) - sql = "EXPLAIN #{to_sql(arel, binds)}" - ExplainPrettyPrinter.new.pp(exec_query(sql, 'EXPLAIN', binds)) - end - - class ExplainPrettyPrinter # :nodoc: - # Pretty prints the result of a EXPLAIN in a way that resembles the output of the - # PostgreSQL shell: - # - # QUERY PLAN - # ------------------------------------------------------------------------------ - # Nested Loop Left Join (cost=0.00..37.24 rows=8 width=0) - # Join Filter: (posts.user_id = users.id) - # -> Index Scan using users_pkey on users (cost=0.00..8.27 rows=1 width=4) - # Index Cond: (id = 1) - # -> Seq Scan on posts (cost=0.00..28.88 rows=8 width=4) - # Filter: (posts.user_id = 1) - # (6 rows) - # - def pp(result) - header = result.columns.first - lines = result.rows.map(&:first) - - # We add 2 because there's one char of padding at both sides, note - # the extra hyphens in the example above. - width = [header, *lines].map(&:length).max + 2 - - pp = [] - - pp << header.center(width).rstrip - pp << '-' * width - - pp += lines.map {|line| " #{line}"} - - nrows = result.rows.length - rows_label = nrows == 1 ? 'row' : 'rows' - pp << "(#{nrows} #{rows_label})" - - pp.join("\n") + "\n" - end - end - - # Executes a SELECT query and returns an array of rows. Each row is an - # array of field values. - def select_rows(sql, name = nil) - select_raw(sql, name).last - end - - # Executes an INSERT query and returns the new record's ID - def insert_sql(sql, name = nil, pk = nil, id_value = nil, sequence_name = nil) - unless pk - # Extract the table from the insert sql. Yuck. - table_ref = extract_table_ref_from_insert_sql(sql) - pk = primary_key(table_ref) if table_ref - end - - if pk - select_value("#{sql} RETURNING #{quote_column_name(pk)}") - else - super - end - end - alias :create :insert - - # create a 2D array representing the result set - def result_as_array(res) #:nodoc: - # check if we have any binary column and if they need escaping - ftypes = Array.new(res.nfields) do |i| - [i, res.ftype(i)] - end - - rows = res.values - return rows unless ftypes.any? { |_, x| - x == BYTEA_COLUMN_TYPE_OID || x == MONEY_COLUMN_TYPE_OID - } - - typehash = ftypes.group_by { |_, type| type } - binaries = typehash[BYTEA_COLUMN_TYPE_OID] || [] - monies = typehash[MONEY_COLUMN_TYPE_OID] || [] - - rows.each do |row| - # unescape string passed BYTEA field (OID == 17) - binaries.each do |index, _| - row[index] = unescape_bytea(row[index]) - end - - # If this is a money type column and there are any currency symbols, - # then strip them off. Indeed it would be prettier to do this in - # PostgreSQLColumn.string_to_decimal but would break form input - # fields that call value_before_type_cast. - monies.each do |index, _| - data = row[index] - # Because money output is formatted according to the locale, there are two - # cases to consider (note the decimal separators): - # (1) $12,345,678.12 - # (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,]/, '').sub!(/,/, '.') - end - end - end - end - - - # Queries the database and returns the results in an Array-like object - def query(sql, name = nil) #:nodoc: - log(sql, name) do - result_as_array @connection.async_exec(sql) - end - end - - # Executes an SQL statement, returning a PGresult object on success - # or raising a PGError exception otherwise. - def execute(sql, name = nil) - log(sql, name) do - @connection.async_exec(sql) - end - end - - def substitute_at(column, index) - Arel::Nodes::BindParam.new "$#{index + 1}" - end - - class Result < ActiveRecord::Result - def initialize(columns, rows, column_types) - super(columns, rows) - @column_types = column_types - end - end - - def exec_query(sql, name = 'SQL', binds = []) - log(sql, name, binds) do - result = binds.empty? ? exec_no_cache(sql, binds) : - exec_cache(sql, binds) - - types = {} - result.fields.each_with_index do |fname, i| - ftype = result.ftype i - fmod = result.fmod i - types[fname] = OID::TYPE_MAP.fetch(ftype, fmod) { |oid, mod| - warn "unknown OID: #{fname}(#{oid}) (#{sql})" - OID::Identity.new - } - end - - ret = Result.new(result.fields, result.values, types) - result.clear - return ret - end - end - - def exec_delete(sql, name = 'SQL', binds = []) - log(sql, name, binds) do - result = binds.empty? ? exec_no_cache(sql, binds) : - exec_cache(sql, binds) - affected = result.cmd_tuples - result.clear - affected - end - end - alias :exec_update :exec_delete - - def sql_for_insert(sql, pk, id_value, sequence_name, binds) - unless pk - # Extract the table from the insert sql. Yuck. - table_ref = extract_table_ref_from_insert_sql(sql) - pk = primary_key(table_ref) if table_ref - end - - sql = "#{sql} RETURNING #{quote_column_name(pk)}" if pk - - [sql, binds] - end - - # Executes an UPDATE query and returns the number of affected tuples. - def update_sql(sql, name = nil) - super.cmd_tuples - end - - # Begins a transaction. - def begin_db_transaction - execute "BEGIN" - end - - # Commits a transaction. - def commit_db_transaction - execute "COMMIT" - end - - # Aborts a transaction. - def rollback_db_transaction - execute "ROLLBACK" - end - - def outside_transaction? - @connection.transaction_status == PGconn::PQTRANS_IDLE - 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 - - # SCHEMA STATEMENTS ======================================== - - # Drops the database specified on the +name+ attribute - # and creates it again using the provided +options+. - def recreate_database(name, options = {}) #:nodoc: - drop_database(name) - create_database(name, options) - end - - # Create a new PostgreSQL database. Options include <tt>:owner</tt>, <tt>:template</tt>, - # <tt>:encoding</tt>, <tt>:tablespace</tt>, and <tt>:connection_limit</tt> (note that MySQL uses - # <tt>:charset</tt> while PostgreSQL uses <tt>:encoding</tt>). - # - # Example: - # create_database config[:database], config - # create_database 'foo_development', :encoding => 'unicode' - def create_database(name, options = {}) - options = options.reverse_merge(:encoding => "utf8") - - option_string = options.symbolize_keys.sum do |key, value| - case key - when :owner - " OWNER = \"#{value}\"" - when :template - " TEMPLATE = \"#{value}\"" - when :encoding - " ENCODING = '#{value}'" - when :tablespace - " TABLESPACE = \"#{value}\"" - when :connection_limit - " CONNECTION LIMIT = #{value}" - else - "" - end - end - - execute "CREATE DATABASE #{quote_table_name(name)}#{option_string}" - end - - # Drops a PostgreSQL database. - # - # Example: - # drop_database 'matt_development' - def drop_database(name) #:nodoc: - execute "DROP DATABASE IF EXISTS #{quote_table_name(name)}" - end - - # Returns the list of all tables in the schema search path or a specified schema. - def tables(name = nil) - query(<<-SQL, 'SCHEMA').map { |row| row[0] } - SELECT tablename - FROM pg_tables - WHERE schemaname = ANY (current_schemas(false)) - SQL - end - - # Returns true if table exists. - # If the schema is not specified as part of +name+ then it will only find tables within - # the current schema search path (regardless of permissions to access tables in other schemas) - def table_exists?(name) - schema, table = Utils.extract_schema_and_table(name.to_s) - return false unless table - - binds = [[nil, table]] - binds << [nil, schema] if schema - - exec_query(<<-SQL, 'SCHEMA', binds).rows.first[0].to_i > 0 - SELECT COUNT(*) - FROM pg_class c - LEFT JOIN pg_namespace n ON n.oid = c.relnamespace - WHERE c.relkind in ('v','r') - AND c.relname = $1 - AND n.nspname = #{schema ? '$2' : 'ANY (current_schemas(false))'} - SQL - end - - # Returns true if schema exists. - def schema_exists?(name) - exec_query(<<-SQL, 'SCHEMA', [[nil, name]]).rows.first[0].to_i > 0 - SELECT COUNT(*) - FROM pg_namespace - WHERE nspname = $1 - SQL - end - - # Returns an array of indexes for the given table. - def indexes(table_name, name = nil) - result = query(<<-SQL, name) - SELECT distinct i.relname, d.indisunique, d.indkey, pg_get_indexdef(d.indexrelid), t.oid - FROM pg_class t - INNER JOIN pg_index d ON t.oid = d.indrelid - INNER JOIN pg_class i ON d.indexrelid = i.oid - WHERE i.relkind = 'i' - AND d.indisprimary = 'f' - AND t.relname = '#{table_name}' - AND i.relnamespace IN (SELECT oid FROM pg_namespace WHERE nspname = ANY (current_schemas(false)) ) - ORDER BY i.relname - SQL - - - result.map do |row| - index_name = row[0] - unique = row[1] == 't' - indkey = row[2].split(" ") - inddef = row[3] - oid = row[4] - - columns = Hash[query(<<-SQL, "Columns for index #{row[0]} on #{table_name}")] - SELECT a.attnum, a.attname - FROM pg_attribute a - WHERE a.attrelid = #{oid} - AND a.attnum IN (#{indkey.join(",")}) - SQL - - column_names = columns.values_at(*indkey).compact - - # add info on sort order for columns (only desc order is explicitly specified, asc is the default) - desc_order_columns = inddef.scan(/(\w+) DESC/).flatten - orders = desc_order_columns.any? ? Hash[desc_order_columns.map {|order_column| [order_column, :desc]}] : {} - where = inddef.scan(/WHERE (.+)$/).flatten[0] - - column_names.empty? ? nil : IndexDefinition.new(table_name, index_name, unique, column_names, [], orders, where) - end.compact - end - - # Returns the list of all column definitions for a table. - def columns(table_name) - # Limit, precision, and scale are all handled by the superclass. - column_definitions(table_name).map do |column_name, type, default, notnull, oid, fmod| - oid = OID::TYPE_MAP.fetch(oid.to_i, fmod.to_i) { - OID::Identity.new - } - PostgreSQLColumn.new(column_name, default, oid, type, notnull == 'f') - end - end - - # Returns the current database name. - def current_database - query('select current_database()')[0][0] - end - - # Returns the current schema name. - def current_schema - query('SELECT current_schema', 'SCHEMA')[0][0] - end - - # Returns the current database encoding format. - def encoding - query(<<-end_sql)[0][0] - SELECT pg_encoding_to_char(pg_database.encoding) FROM pg_database - WHERE pg_database.datname LIKE '#{current_database}' - end_sql - end - - # Sets the schema search path to a string of comma-separated schema names. - # Names beginning with $ have to be quoted (e.g. $user => '$user'). - # See: http://www.postgresql.org/docs/current/static/ddl-schemas.html - # - # This should be not be called manually but set in database.yml. - def schema_search_path=(schema_csv) - if schema_csv - execute "SET search_path TO #{schema_csv}" - @schema_search_path = schema_csv - end - end - - # Returns the active schema search path. - def schema_search_path - @schema_search_path ||= query('SHOW search_path', 'SCHEMA')[0][0] - end - - # Returns the current client message level. - def client_min_messages - query('SHOW client_min_messages', 'SCHEMA')[0][0] - end - - # Set the client message level. - def client_min_messages=(level) - execute("SET client_min_messages TO '#{level}'", 'SCHEMA') - end - - # Returns the sequence name for a table's primary key or some other specified key. - def default_sequence_name(table_name, pk = nil) #:nodoc: - serial_sequence(table_name, pk || 'id').split('.').last - rescue ActiveRecord::StatementInvalid - "#{table_name}_#{pk || 'id'}_seq" - end - - def serial_sequence(table, column) - result = exec_query(<<-eosql, 'SCHEMA', [[nil, table], [nil, column]]) - SELECT pg_get_serial_sequence($1, $2) - eosql - result.rows.first.first - end - - # Resets the sequence of a table's primary key to the maximum value. - def reset_pk_sequence!(table, pk = nil, sequence = nil) #:nodoc: - unless pk and sequence - default_pk, default_sequence = pk_and_sequence_for(table) - - pk ||= default_pk - sequence ||= default_sequence - end - - if @logger && pk && !sequence - @logger.warn "#{table} has primary key #{pk} with no default sequence" - end - - if pk && sequence - quoted_sequence = quote_table_name(sequence) - - select_value <<-end_sql, 'Reset sequence' - SELECT setval('#{quoted_sequence}', (SELECT COALESCE(MAX(#{quote_column_name pk})+(SELECT increment_by FROM #{quoted_sequence}), (SELECT min_value FROM #{quoted_sequence})) FROM #{quote_table_name(table)}), false) - end_sql - end - end - - # Returns a table's primary key and belonging sequence. - 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] - SELECT attr.attname, seq.relname - FROM pg_class seq, - pg_attribute attr, - pg_depend dep, - pg_namespace name, - pg_constraint cons - WHERE seq.oid = dep.objid - AND seq.relkind = 'S' - AND attr.attrelid = dep.refobjid - AND attr.attnum = dep.refobjsubid - AND attr.attrelid = cons.conrelid - AND attr.attnum = cons.conkey[1] - AND cons.contype = 'p' - AND dep.refobjid = '#{quote_table_name(table)}'::regclass - end_sql - - if result.nil? or result.empty? - # If that fails, try parsing the primary key's default value. - # Support the 7.x and 8.0 nextval('foo'::text) as well as - # the 8.1+ nextval('foo'::regclass). - result = query(<<-end_sql, 'PK and custom sequence')[0] - SELECT attr.attname, - CASE - WHEN split_part(def.adsrc, '''', 2) ~ '.' THEN - substr(split_part(def.adsrc, '''', 2), - strpos(split_part(def.adsrc, '''', 2), '.')+1) - ELSE split_part(def.adsrc, '''', 2) - END - FROM pg_class t - JOIN pg_attribute attr ON (t.oid = attrelid) - JOIN pg_attrdef def ON (adrelid = attrelid AND adnum = attnum) - JOIN pg_constraint cons ON (conrelid = adrelid AND adnum = conkey[1]) - WHERE t.oid = '#{quote_table_name(table)}'::regclass - AND cons.contype = 'p' - AND def.adsrc ~* 'nextval' - end_sql - end - - [result.first, result.last] - rescue - nil - end - - # Returns just a table's primary key - def primary_key(table) - row = exec_query(<<-end_sql, 'SCHEMA', [[nil, table]]).rows.first - SELECT DISTINCT(attr.attname) - FROM pg_attribute attr - INNER JOIN pg_depend dep ON attr.attrelid = dep.refobjid AND attr.attnum = dep.refobjsubid - INNER JOIN pg_constraint cons ON attr.attrelid = cons.conrelid AND attr.attnum = cons.conkey[1] - WHERE cons.contype = 'p' - AND dep.refobjid = $1::regclass - end_sql - - row && row.first - end - - # Renames a table. - # - # Example: - # rename_table('octopuses', 'octopi') - def rename_table(name, new_name) - clear_cache! - execute "ALTER TABLE #{quote_table_name(name)} RENAME TO #{quote_table_name(new_name)}" - end - - # 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 = {}) - clear_cache! - 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) - - execute add_column_sql - end - - # Changes the column of a table. - def change_column(table_name, column_name, type, options = {}) - clear_cache! - quoted_table_name = quote_table_name(table_name) - - execute "ALTER TABLE #{quoted_table_name} ALTER COLUMN #{quote_column_name(column_name)} TYPE #{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 - - # Changes the default value of a table column. - def change_column_default(table_name, column_name, default) - clear_cache! - execute "ALTER TABLE #{quote_table_name(table_name)} ALTER COLUMN #{quote_column_name(column_name)} SET DEFAULT #{quote(default)}" - end - - def change_column_null(table_name, column_name, null, default = nil) - clear_cache! - unless null || default.nil? - execute("UPDATE #{quote_table_name(table_name)} SET #{quote_column_name(column_name)}=#{quote(default)} WHERE #{quote_column_name(column_name)} IS NULL") - end - execute("ALTER TABLE #{quote_table_name(table_name)} ALTER #{quote_column_name(column_name)} #{null ? 'DROP' : 'SET'} NOT NULL") - end - - # Renames a column in a table. - def rename_column(table_name, column_name, new_column_name) - clear_cache! - execute "ALTER TABLE #{quote_table_name(table_name)} RENAME COLUMN #{quote_column_name(column_name)} TO #{quote_column_name(new_column_name)}" - end - - def remove_index!(table_name, index_name) #:nodoc: - 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 - - # Maps logical Rails types to PostgreSQL-specific data types. - def type_to_sql(type, limit = nil, precision = nil, scale = nil) - return super unless type.to_s == 'integer' - return 'integer' unless limit - - case limit - when 1, 2; 'smallint' - when 3, 4; 'integer' - when 5..8; 'bigint' - else raise(ActiveRecordError, "No integer type has byte size #{limit}. Use a numeric with precision 0 instead.") - end - end - - # Returns a SELECT DISTINCT clause for a given set of columns and a given ORDER BY clause. - # - # PostgreSQL requires the ORDER BY columns in the select list for distinct queries, and - # requires that the ORDER BY include the distinct column. - # - # distinct("posts.id", "posts.created_at desc") - 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 = orders.collect { |s| s.gsub(/\s+(ASC|DESC)\s*/i, '') } - 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}" } - - "DISTINCT #{columns}, #{order_columns * ', '}" - end - module Utils extend self @@ -1215,17 +567,26 @@ module ActiveRecord end end + def use_insert_returning? + @use_insert_returning + end + protected + # Returns the version of the connected PostgreSQL server. def postgresql_version @connection.server_version end + # See http://www.postgresql.org/docs/9.1/static/errcodes-appendix.html + FOREIGN_KEY_VIOLATION = "23503" + UNIQUE_VIOLATION = "23505" + def translate_exception(exception, message) - case exception.message - when /duplicate key value violates unique constraint/ + case exception.result.try(:error_field, PGresult::PG_DIAG_SQLSTATE) + when UNIQUE_VIOLATION RecordNotUnique.new(message, exception) - when /violates foreign key constraint/ + when FOREIGN_KEY_VIOLATION InvalidForeignKey.new(message, exception) else super @@ -1233,21 +594,30 @@ module ActiveRecord end private - def initialize_type_map - result = execute('SELECT oid, typname, typelem, typdelim FROM pg_type', 'SCHEMA') - leaves, nodes = result.partition { |row| row['typelem'] == '0' } - # populate the leaf nodes - leaves.find_all { |row| OID.registered_type? row['typname'] }.each do |row| - OID::TYPE_MAP[row['oid'].to_i] = OID::NAMES[row['typname']] - end + def initialize_type_map + result = execute('SELECT oid, typname, typelem, typdelim, typinput FROM pg_type', 'SCHEMA') + leaves, nodes = result.partition { |row| row['typelem'] == '0' } + + # populate the leaf nodes + leaves.find_all { |row| OID.registered_type? row['typname'] }.each do |row| + OID::TYPE_MAP[row['oid'].to_i] = OID::NAMES[row['typname']] + end - # populate composite types - nodes.find_all { |row| OID::TYPE_MAP.key? row['typelem'].to_i }.each do |row| - vector = OID::Vector.new row['typdelim'], OID::TYPE_MAP[row['typelem'].to_i] - OID::TYPE_MAP[row['oid'].to_i] = vector + arrays, nodes = nodes.partition { |row| row['typinput'] == 'array_in' } + + # populate composite types + nodes.find_all { |row| OID::TYPE_MAP.key? row['typelem'].to_i }.each do |row| + vector = OID::Vector.new row['typdelim'], OID::TYPE_MAP[row['typelem'].to_i] + OID::TYPE_MAP[row['oid'].to_i] = vector + end + + # populate array types + arrays.find_all { |row| OID::TYPE_MAP.key? row['typelem'].to_i }.each do |row| + array = OID::Array.new OID::TYPE_MAP[row['typelem'].to_i] + OID::TYPE_MAP[row['oid'].to_i] = array + end end - end FEATURE_NOT_SUPPORTED = "0A000" # :nodoc: @@ -1327,7 +697,7 @@ module ActiveRecord if @config[:encoding] @connection.set_client_encoding(@config[:encoding]) end - self.client_min_messages = @config[:min_messages] if @config[:min_messages] + self.client_min_messages = @config[:min_messages] || 'warning' self.schema_search_path = @config[:schema_search_path] || @config[:schema_order] # Use standard-conforming strings if available so we don't have to do the E'...' dance. @@ -1344,8 +714,15 @@ module ActiveRecord # Returns the current ID of a table's sequence. def last_insert_id(sequence_name) #:nodoc: - r = exec_query("SELECT currval($1)", 'SQL', [[nil, sequence_name]]) - Integer(r.rows.first.first) + Integer(last_insert_id_value(sequence_name)) + end + + def last_insert_id_value(sequence_name) + last_insert_id_result(sequence_name).rows.first.first + end + + def last_insert_id_result(sequence_name) #:nodoc: + exec_query("SELECT currval('#{sequence_name}')", 'SQL') end # Executes a SELECT query and returns the results, performing any data type @@ -1382,12 +759,12 @@ module ActiveRecord # - ::regclass is a function that gives the id for a table name def column_definitions(table_name) #:nodoc: exec_query(<<-end_sql, 'SCHEMA').rows - SELECT a.attname, format_type(a.atttypid, a.atttypmod), d.adsrc, a.attnotnull, a.atttypid, a.atttypmod - FROM pg_attribute a LEFT JOIN pg_attrdef d - ON a.attrelid = d.adrelid AND a.attnum = d.adnum - WHERE a.attrelid = '#{quote_table_name(table_name)}'::regclass - AND a.attnum > 0 AND NOT a.attisdropped - ORDER BY a.attnum + SELECT a.attname, format_type(a.atttypid, a.atttypmod), d.adsrc, a.attnotnull, a.atttypid, a.atttypmod + FROM pg_attribute a LEFT JOIN pg_attrdef d + ON a.attrelid = d.adrelid AND a.attnum = d.adnum + WHERE a.attrelid = '#{quote_table_name(table_name)}'::regclass + AND a.attnum > 0 AND NOT a.attisdropped + ORDER BY a.attnum end_sql end diff --git a/activerecord/lib/active_record/connection_adapters/schema_cache.rb b/activerecord/lib/active_record/connection_adapters/schema_cache.rb index 962718da56..aad1f9a7ef 100644 --- a/activerecord/lib/active_record/connection_adapters/schema_cache.rb +++ b/activerecord/lib/active_record/connection_adapters/schema_cache.rb @@ -1,26 +1,17 @@ module ActiveRecord module ConnectionAdapters class SchemaCache - attr_reader :columns, :columns_hash, :primary_keys, :tables - attr_reader :connection + attr_reader :columns, :columns_hash, :primary_keys, :tables, :version + attr_accessor :connection def initialize(conn) @connection = conn - @tables = {} - @columns = Hash.new do |h, table_name| - h[table_name] = conn.columns(table_name) - 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] = table_exists?(table_name) ? conn.primary_key(table_name) : nil - end + @columns = {} + @columns_hash = {} + @primary_keys = {} + @tables = {} + prepare_default_proc end # A cached lookup for table existence. @@ -30,12 +21,22 @@ module ActiveRecord @tables[name] = connection.table_exists?(name) end + # Add internal cache for table with +table_name+. + def add(table_name) + if table_exists?(table_name) + @primary_keys[table_name] + @columns[table_name] + @columns_hash[table_name] + end + end + # Clears out internal caches def clear! @columns.clear @columns_hash.clear @primary_keys.clear @tables.clear + @version = nil end # Clear out internal caches for table with +table_name+. @@ -45,6 +46,37 @@ module ActiveRecord @primary_keys.delete table_name @tables.delete table_name end + + def marshal_dump + # if we get current version during initialization, it happens stack over flow. + @version = ActiveRecord::Migrator.current_version + [@version] + [:@columns, :@columns_hash, :@primary_keys, :@tables].map do |val| + self.instance_variable_get(val).inject({}) { |h, v| h[v[0]] = v[1]; h } + end + end + + def marshal_load(array) + @version, @columns, @columns_hash, @primary_keys, @tables = array + prepare_default_proc + end + + private + + def prepare_default_proc + @columns.default_proc = Proc.new do |h, table_name| + h[table_name] = connection.columns(table_name) + end + + @columns_hash.default_proc = Proc.new do |h, table_name| + h[table_name] = Hash[columns[table_name].map { |col| + [col.name, col] + }] + end + + @primary_keys.default_proc = Proc.new do |h, table_name| + h[table_name] = table_exists?(table_name) ? connection.primary_key(table_name) : nil + end + end end end end diff --git a/activerecord/lib/active_record/connection_adapters/sqlite3_adapter.rb b/activerecord/lib/active_record/connection_adapters/sqlite3_adapter.rb index ee5d10859c..4a48812807 100644 --- a/activerecord/lib/active_record/connection_adapters/sqlite3_adapter.rb +++ b/activerecord/lib/active_record/connection_adapters/sqlite3_adapter.rb @@ -1,6 +1,8 @@ -require 'active_record/connection_adapters/sqlite_adapter' +require 'active_record/connection_adapters/abstract_adapter' +require 'active_record/connection_adapters/statement_pool' +require 'arel/visitors/bind_visitor' -gem 'sqlite3', '~> 1.3.5' +gem 'sqlite3', '~> 1.3.6' require 'sqlite3' module ActiveRecord @@ -19,10 +21,6 @@ module ActiveRecord config[:database] = File.expand_path(config[:database], Rails.root) end - unless 'sqlite3' == config[:adapter] - raise ArgumentError, 'adapter name should be "sqlite3"' - end - db = SQLite3::Database.new( config[:database], :results_as_hash => true @@ -35,7 +33,189 @@ module ActiveRecord end module ConnectionAdapters #:nodoc: - class SQLite3Adapter < SQLiteAdapter # :nodoc: + class SQLite3Column < Column #:nodoc: + class << self + def binary_to_string(value) + if value.encoding != Encoding::ASCII_8BIT + value = value.force_encoding(Encoding::ASCII_8BIT) + end + value + end + end + end + + # The SQLite3 adapter works SQLite 3.6.16 or newer + # with the sqlite3-ruby drivers (available as gem from https://rubygems.org/gems/sqlite3). + # + # Options: + # + # * <tt>:database</tt> - Path to the database file. + class SQLite3Adapter < AbstractAdapter + class Version + include Comparable + + def initialize(version_string) + @version = version_string.split('.').map { |v| v.to_i } + end + + def <=>(version_string) + @version <=> version_string.split('.').map { |v| v.to_i } + end + end + + class StatementPool < ConnectionAdapters::StatementPool + def initialize(connection, max) + super + @cache = Hash.new { |h,pid| h[pid] = {} } + end + + def each(&block); cache.each(&block); end + def key?(key); cache.key?(key); end + def [](key); cache[key]; end + def length; cache.length; end + + def []=(sql, key) + while @max <= cache.size + dealloc(cache.shift.last[:stmt]) + end + cache[sql] = key + end + + def clear + cache.values.each do |hash| + dealloc hash[:stmt] + end + cache.clear + end + + private + def cache + @cache[$$] + end + + def dealloc(stmt) + stmt.close unless stmt.closed? + end + end + + class BindSubstitution < Arel::Visitors::SQLite # :nodoc: + include Arel::Visitors::BindVisitor + end + + def initialize(connection, logger, config) + super(connection, logger) + + @active = nil + @statements = StatementPool.new(@connection, + config.fetch(:statement_limit) { 1000 }) + @config = config + + if config.fetch(:prepared_statements) { true } + @visitor = Arel::Visitors::SQLite.new self + else + @visitor = BindSubstitution.new self + end + end + + def adapter_name #:nodoc: + 'SQLite' + end + + # Returns true + def supports_ddl_transactions? + true + end + + # Returns true if SQLite version is '3.6.8' or greater, false otherwise. + def supports_savepoints? + sqlite_version >= '3.6.8' + end + + # Returns true, since this connection adapter supports prepared statement + # caching. + def supports_statement_cache? + true + end + + # Returns true, since this connection adapter supports migrations. + def supports_migrations? #:nodoc: + true + end + + # Returns true. + def supports_primary_key? #:nodoc: + true + end + + def requires_reloading? + true + end + + # Returns true + def supports_add_column? + true + end + + def active? + @active != false + end + + # Disconnects from the database if already connected. Otherwise, this + # method does nothing. + def disconnect! + super + @active = false + @connection.close rescue nil + end + + # Clears the prepared statements cache. + def clear_cache! + @statements.clear + end + + # Returns true + def supports_count_distinct? #:nodoc: + true + end + + # Returns true + def supports_autoincrement? #:nodoc: + true + end + + def supports_index_sort_order? + true + end + + def native_database_types #:nodoc: + { + :primary_key => default_primary_key_type, + :string => { :name => "varchar", :limit => 255 }, + :text => { :name => "text" }, + :integer => { :name => "integer" }, + :float => { :name => "float" }, + :decimal => { :name => "decimal" }, + :datetime => { :name => "datetime" }, + :timestamp => { :name => "datetime" }, + :time => { :name => "time" }, + :date => { :name => "date" }, + :binary => { :name => "blob" }, + :boolean => { :name => "boolean" } + } + end + + # Returns the current database encoding format as a string, eg: 'UTF-8' + def encoding + @connection.encoding.to_s + end + + # Returns true. + def supports_explain? + true + end + + # QUOTING ================================================== + def quote(value, column = nil) if value.kind_of?(String) && column && column.type == :binary && column.class.respond_to?(:string_to_binary) s = column.class.string_to_binary(value).unpack("H*")[0] @@ -45,11 +225,391 @@ module ActiveRecord end end - # Returns the current database encoding format as a string, eg: 'UTF-8' - def encoding - @connection.encoding.to_s + def quote_string(s) #:nodoc: + @connection.class.quote(s) + end + + def quote_column_name(name) #:nodoc: + %Q("#{name.to_s.gsub('"', '""')}") + end + + # 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.respond_to?(:usec) + "#{super}.#{sprintf("%06d", value.usec)}" + else + super + end + end + + def type_cast(value, column) # :nodoc: + return value.to_f if BigDecimal === value + return super unless String === value + return super unless column && value + + value = super + if column.type == :string && value.encoding == Encoding::ASCII_8BIT + logger.error "Binary data inserted for `string` type on column `#{column.name}`" if logger + value.encode! 'utf-8' + end + value + end + + # DATABASE STATEMENTS ====================================== + + def explain(arel, binds = []) + sql = "EXPLAIN QUERY PLAN #{to_sql(arel, binds)}" + ExplainPrettyPrinter.new.pp(exec_query(sql, 'EXPLAIN', binds)) + end + + class ExplainPrettyPrinter + # Pretty prints the result of a EXPLAIN QUERY PLAN in a way that resembles + # the output of the SQLite shell: + # + # 0|0|0|SEARCH TABLE users USING INTEGER PRIMARY KEY (rowid=?) (~1 rows) + # 0|1|1|SCAN TABLE posts (~100000 rows) + # + def pp(result) # :nodoc: + result.rows.map do |row| + row.join('|') + end.join("\n") + "\n" + end + end + + 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| + type_cast(val, col) + } + end + + ActiveRecord::Result.new(cols, stmt.to_a) + end end + def exec_delete(sql, name = 'SQL', binds = []) + exec_query(sql, name, binds) + @connection.changes + end + alias :exec_update :exec_delete + + def last_inserted_id(result) + @connection.last_insert_row_id + end + + def execute(sql, name = nil) #:nodoc: + log(sql, name) { @connection.execute(sql) } + end + + def update_sql(sql, name = nil) #:nodoc: + super + @connection.changes + end + + def delete_sql(sql, name = nil) #:nodoc: + sql += " WHERE 1=1" unless sql =~ /WHERE/i + super sql, name + end + + def insert_sql(sql, name = nil, pk = nil, id_value = nil, sequence_name = nil) #:nodoc: + super + id_value || @connection.last_insert_row_id + end + alias :create :insert_sql + + def select_rows(sql, name = nil) + 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: + log('begin transaction',nil) { @connection.transaction } + end + + def commit_db_transaction #:nodoc: + log('commit transaction',nil) { @connection.commit } + end + + def rollback_db_transaction #:nodoc: + log('rollback transaction',nil) { @connection.rollback } + end + + # SCHEMA STATEMENTS ======================================== + + def tables(name = nil, table_name = nil) #:nodoc: + sql = <<-SQL + SELECT name + FROM sqlite_master + WHERE type = 'table' AND NOT name = 'sqlite_sequence' + SQL + sql << " AND name = #{quote_table_name(table_name)}" if table_name + + exec_query(sql, 'SCHEMA').map do |row| + row['name'] + end + end + + def table_exists?(table_name) + table_name && tables(nil, table_name).any? + end + + # Returns an array of +SQLite3Column+ objects for the table specified by +table_name+. + def columns(table_name) #:nodoc: + table_structure(table_name).map do |field| + case field["dflt_value"] + when /^null$/i + field["dflt_value"] = nil + when /^'(.*)'$/m + field["dflt_value"] = $1.gsub("''", "'") + when /^"(.*)"$/m + field["dflt_value"] = $1.gsub('""', '"') + end + + SQLite3Column.new(field['name'], field['dflt_value'], field['type'], field['notnull'].to_i == 0) + end + end + + # Returns an array of indexes for the given table. + def indexes(table_name, name = nil) #:nodoc: + exec_query("PRAGMA index_list(#{quote_table_name(table_name)})", 'SCHEMA').map do |row| + IndexDefinition.new( + table_name, + row['name'], + row['unique'] != 0, + exec_query("PRAGMA index_info('#{row['name']}')", "SCHEMA").map { |col| + col['name'] + }) + end + end + + def primary_key(table_name) #:nodoc: + column = table_structure(table_name).find { |field| + field['pk'] == 1 + } + column && column['name'] + end + + def remove_index!(table_name, index_name) #:nodoc: + exec_query "DROP INDEX #{quote_column_name(index_name)}" + end + + # Renames a table. + # + # Example: + # rename_table('octopuses', 'octopi') + def rename_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 + # SQLite has an additional restriction on the ALTER TABLE statement + def valid_alter_table_options( type, options) + type.to_sym != :primary_key + end + + def add_column(table_name, column_name, type, options = {}) #:nodoc: + if supports_add_column? && valid_alter_table_options( type, options ) + super(table_name, column_name, type, options) + else + alter_table(table_name) do |definition| + definition.column(column_name, type, options) + end + end + end + + def remove_column(table_name, *column_names) #:nodoc: + raise ArgumentError.new("You must specify at least one column name. Example: remove_column(:people, :first_name)") if column_names.empty? + column_names.each do |column_name| + alter_table(table_name) do |definition| + definition.columns.delete(definition[column_name]) + end + end + end + alias :remove_columns :remove_column + + def change_column_default(table_name, column_name, default) #:nodoc: + alter_table(table_name) do |definition| + definition[column_name].default = default + end + end + + def change_column_null(table_name, column_name, null, default = nil) + unless null || default.nil? + 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 + end + end + + def change_column(table_name, column_name, type, options = {}) #:nodoc: + alter_table(table_name) do |definition| + include_default = options_include_default?(options) + definition[column_name].instance_eval do + self.type = type + self.limit = options[:limit] if options.include?(:limit) + self.default = options[:default] if include_default + self.null = options[:null] if options.include?(:null) + self.precision = options[:precision] if options.include?(:precision) + self.scale = options[:scale] if options.include?(:scale) + end + end + end + + def rename_column(table_name, column_name, new_column_name) #:nodoc: + unless columns(table_name).detect{|c| c.name == column_name.to_s } + raise ActiveRecord::ActiveRecordError, "Missing column #{table_name}.#{column_name}" + end + alter_table(table_name, :rename => {column_name.to_s => new_column_name.to_s}) + end + + def empty_insert_statement_value + "VALUES(NULL)" + end + + protected + def select(sql, name = nil, binds = []) #:nodoc: + exec_query(sql, name, binds) + end + + def table_structure(table_name) + structure = exec_query("PRAGMA table_info(#{quote_table_name(table_name)})", 'SCHEMA').to_hash + raise(ActiveRecord::StatementInvalid, "Could not find table '#{table_name}'") if structure.empty? + structure + end + + def alter_table(table_name, options = {}) #:nodoc: + altered_table_name = "altered_#{table_name}" + caller = lambda {|definition| yield definition if block_given?} + + transaction do + move_table(table_name, altered_table_name, + options.merge(:temporary => true)) + move_table(altered_table_name, table_name, &caller) + end + end + + def move_table(from, to, options = {}, &block) #:nodoc: + copy_table(from, to, options, &block) + drop_table(from) + end + + def copy_table(from, to, options = {}) #:nodoc: + from_primary_key = primary_key(from) + options[:primary_key] = from_primary_key if from_primary_key != 'id' + unless options[:primary_key] + options[:id] = columns(from).detect{|c| c.name == 'id'}.present? && from_primary_key == 'id' + end + create_table(to, options) do |definition| + @definition = definition + columns(from).each do |column| + column_name = options[:rename] ? + (options[:rename][column.name] || + options[:rename][column.name.to_sym] || + column.name) : column.name + + @definition.column(column_name, column.type, + :limit => column.limit, :default => column.default, + :precision => column.precision, :scale => column.scale, + :null => column.null) + end + @definition.primary_key(from_primary_key) if from_primary_key + yield @definition if block_given? + end + + copy_table_indexes(from, to, options[:rename] || {}) + copy_table_contents(from, to, + @definition.columns.map {|column| column.name}, + options[:rename] || {}) + end + + def copy_table_indexes(from, to, rename = {}) #:nodoc: + indexes(from).each do |index| + name = index.name + if to == "altered_#{from}" + name = "temp_#{name}" + elsif from == "altered_#{to}" + name = name[5..-1] + end + + 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 + + unless columns.empty? + # index name can't be the same + opts = { :name => name.gsub(/_(#{from})_/, "_#{to}_") } + opts[:unique] = true if index.unique + add_index(to, columns, opts) + end + end + end + + def copy_table_contents(from, to, columns, rename = {}) #:nodoc: + 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) + 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 << ')' + exec_query sql + end + end + + def sqlite_version + @sqlite_version ||= SQLite3Adapter::Version.new(select_value('select sqlite_version(*)')) + end + + def default_primary_key_type + if supports_autoincrement? + 'INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL' + else + 'INTEGER PRIMARY KEY NOT NULL' + end + end + + def translate_exception(exception, message) + case exception.message + when /column(s)? .* (is|are) not unique/ + RecordNotUnique.new(message, exception) + else + super + end + end + end end end diff --git a/activerecord/lib/active_record/connection_adapters/sqlite_adapter.rb b/activerecord/lib/active_record/connection_adapters/sqlite_adapter.rb deleted file mode 100644 index c85c4c6e51..0000000000 --- a/activerecord/lib/active_record/connection_adapters/sqlite_adapter.rb +++ /dev/null @@ -1,563 +0,0 @@ -require 'active_record/connection_adapters/abstract_adapter' -require 'active_record/connection_adapters/statement_pool' -require 'arel/visitors/bind_visitor' - -module ActiveRecord - module ConnectionAdapters #:nodoc: - class SQLiteColumn < Column #:nodoc: - class << self - def binary_to_string(value) - if value.encoding != Encoding::ASCII_8BIT - value = value.force_encoding(Encoding::ASCII_8BIT) - end - value - end - end - end - - # 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: - # - # * <tt>:database</tt> - Path to the database file. - class SQLiteAdapter < AbstractAdapter - class Version - include Comparable - - def initialize(version_string) - @version = version_string.split('.').map { |v| v.to_i } - end - - def <=>(version_string) - @version <=> version_string.split('.').map { |v| v.to_i } - end - end - - class StatementPool < ConnectionAdapters::StatementPool - def initialize(connection, max) - super - @cache = Hash.new { |h,pid| h[pid] = {} } - end - - def each(&block); cache.each(&block); end - def key?(key); cache.key?(key); end - def [](key); cache[key]; end - def length; cache.length; end - - def []=(sql, key) - while @max <= cache.size - dealloc(cache.shift.last[:stmt]) - end - cache[sql] = key - end - - def clear - cache.values.each do |hash| - dealloc hash[:stmt] - end - cache.clear - end - - private - def cache - @cache[$$] - end - - def dealloc(stmt) - stmt.close unless stmt.closed? - end - end - - class BindSubstitution < Arel::Visitors::SQLite # :nodoc: - include Arel::Visitors::BindVisitor - end - - def initialize(connection, logger, config) - super(connection, logger) - @statements = StatementPool.new(@connection, - config.fetch(:statement_limit) { 1000 }) - @config = config - - if config.fetch(:prepared_statements) { true } - @visitor = Arel::Visitors::SQLite.new self - else - @visitor = BindSubstitution.new self - end - end - - def adapter_name #:nodoc: - 'SQLite' - end - - # Returns true if SQLite version is '2.0.0' or greater, false otherwise. - def supports_ddl_transactions? - sqlite_version >= '2.0.0' - end - - # Returns true if SQLite version is '3.6.8' or greater, false otherwise. - def supports_savepoints? - sqlite_version >= '3.6.8' - end - - # Returns true, since this connection adapter supports prepared statement - # caching. - def supports_statement_cache? - true - end - - # Returns true, since this connection adapter supports migrations. - def supports_migrations? #:nodoc: - true - end - - # Returns true. - def supports_primary_key? #:nodoc: - true - end - - # Returns true. - def supports_explain? - true - end - - def requires_reloading? - true - end - - # Returns true if SQLite version is '3.1.6' or greater, false otherwise. - def supports_add_column? - sqlite_version >= '3.1.6' - end - - # Disconnects from the database if already connected. Otherwise, this - # method does nothing. - def disconnect! - super - clear_cache! - @connection.close rescue nil - end - - # Clears the prepared statements cache. - def clear_cache! - @statements.clear - end - - # Returns true if SQLite version is '3.2.6' or greater, false otherwise. - def supports_count_distinct? #:nodoc: - sqlite_version >= '3.2.6' - end - - # Returns true if SQLite version is '3.1.0' or greater, false otherwise. - def supports_autoincrement? #:nodoc: - sqlite_version >= '3.1.0' - end - - def supports_index_sort_order? - sqlite_version >= '3.3.0' - end - - def native_database_types #:nodoc: - { - :primary_key => default_primary_key_type, - :string => { :name => "varchar", :limit => 255 }, - :text => { :name => "text" }, - :integer => { :name => "integer" }, - :float => { :name => "float" }, - :decimal => { :name => "decimal" }, - :datetime => { :name => "datetime" }, - :timestamp => { :name => "datetime" }, - :time => { :name => "time" }, - :date => { :name => "date" }, - :binary => { :name => "blob" }, - :boolean => { :name => "boolean" } - } - end - - - # QUOTING ================================================== - - def quote_string(s) #:nodoc: - @connection.class.quote(s) - end - - def quote_column_name(name) #:nodoc: - %Q("#{name.to_s.gsub('"', '""')}") - end - - # 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.respond_to?(:usec) - "#{super}.#{sprintf("%06d", value.usec)}" - else - super - end - end - - def type_cast(value, column) # :nodoc: - return value.to_f if BigDecimal === value - return super unless String === value - return super unless column && value - - value = super - if column.type == :string && value.encoding == Encoding::ASCII_8BIT - @logger.error "Binary data inserted for `string` type on column `#{column.name}`" - value.encode! 'utf-8' - end - value - end - - # DATABASE STATEMENTS ====================================== - - def explain(arel, binds = []) - sql = "EXPLAIN QUERY PLAN #{to_sql(arel, binds)}" - ExplainPrettyPrinter.new.pp(exec_query(sql, 'EXPLAIN', binds)) - end - - class ExplainPrettyPrinter - # Pretty prints the result of a EXPLAIN QUERY PLAN in a way that resembles - # the output of the SQLite shell: - # - # 0|0|0|SEARCH TABLE users USING INTEGER PRIMARY KEY (rowid=?) (~1 rows) - # 0|1|1|SCAN TABLE posts (~100000 rows) - # - def pp(result) # :nodoc: - result.rows.map do |row| - row.join('|') - end.join("\n") + "\n" - end - end - - 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| - type_cast(val, col) - } - end - - ActiveRecord::Result.new(cols, stmt.to_a) - end - end - - def exec_delete(sql, name = 'SQL', binds = []) - exec_query(sql, name, binds) - @connection.changes - end - alias :exec_update :exec_delete - - def last_inserted_id(result) - @connection.last_insert_row_id - end - - def execute(sql, name = nil) #:nodoc: - log(sql, name) { @connection.execute(sql) } - end - - def update_sql(sql, name = nil) #:nodoc: - super - @connection.changes - end - - def delete_sql(sql, name = nil) #:nodoc: - sql += " WHERE 1=1" unless sql =~ /WHERE/i - super sql, name - end - - def insert_sql(sql, name = nil, pk = nil, id_value = nil, sequence_name = nil) #:nodoc: - super - id_value || @connection.last_insert_row_id - end - alias :create :insert_sql - - def select_rows(sql, name = nil) - 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: - log('begin transaction',nil) { @connection.transaction } - end - - def commit_db_transaction #:nodoc: - log('commit transaction',nil) { @connection.commit } - end - - def rollback_db_transaction #:nodoc: - log('rollback transaction',nil) { @connection.rollback } - end - - # SCHEMA STATEMENTS ======================================== - - def tables(name = 'SCHEMA', table_name = nil) #:nodoc: - sql = <<-SQL - SELECT name - FROM sqlite_master - WHERE type = 'table' AND NOT name = 'sqlite_sequence' - SQL - sql << " AND name = #{quote_table_name(table_name)}" if table_name - - exec_query(sql, name).map do |row| - row['name'] - end - end - - def table_exists?(name) - name && tables('SCHEMA', name).any? - end - - # Returns an array of +SQLiteColumn+ objects for the table specified by +table_name+. - def columns(table_name) #: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 - - # Returns an array of indexes for the given table. - def indexes(table_name, name = nil) #:nodoc: - exec_query("PRAGMA index_list(#{quote_table_name(table_name)})", name).map do |row| - IndexDefinition.new( - table_name, - row['name'], - row['unique'] != 0, - exec_query("PRAGMA index_info('#{row['name']}')").map { |col| - col['name'] - }) - end - end - - def primary_key(table_name) #:nodoc: - column = table_structure(table_name).find { |field| - field['pk'] == 1 - } - column && column['name'] - end - - def remove_index!(table_name, index_name) #:nodoc: - exec_query "DROP INDEX #{quote_column_name(index_name)}" - end - - # Renames a table. - # - # Example: - # rename_table('octopuses', 'octopi') - def rename_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 - # SQLite has an additional restriction on the ALTER TABLE statement - def valid_alter_table_options( type, options) - type.to_sym != :primary_key - end - - def add_column(table_name, column_name, type, options = {}) #:nodoc: - if supports_add_column? && valid_alter_table_options( type, options ) - super(table_name, column_name, type, options) - else - alter_table(table_name) do |definition| - definition.column(column_name, type, options) - end - end - end - - def remove_column(table_name, *column_names) #:nodoc: - 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| - alter_table(table_name) do |definition| - definition.columns.delete(definition[column_name]) - end - end - end - alias :remove_columns :remove_column - - def change_column_default(table_name, column_name, default) #:nodoc: - alter_table(table_name) do |definition| - definition[column_name].default = default - end - end - - def change_column_null(table_name, column_name, null, default = nil) - unless null || default.nil? - 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 - end - end - - def change_column(table_name, column_name, type, options = {}) #:nodoc: - alter_table(table_name) do |definition| - include_default = options_include_default?(options) - definition[column_name].instance_eval do - self.type = type - self.limit = options[:limit] if options.include?(:limit) - self.default = options[:default] if include_default - self.null = options[:null] if options.include?(:null) - self.precision = options[:precision] if options.include?(:precision) - self.scale = options[:scale] if options.include?(:scale) - end - end - end - - def rename_column(table_name, column_name, new_column_name) #:nodoc: - unless columns(table_name).detect{|c| c.name == column_name.to_s } - raise ActiveRecord::ActiveRecordError, "Missing column #{table_name}.#{column_name}" - end - alter_table(table_name, :rename => {column_name.to_s => new_column_name.to_s}) - end - - def empty_insert_statement_value - "VALUES(NULL)" - end - - protected - def select(sql, name = nil, binds = []) #:nodoc: - exec_query(sql, name, binds) - end - - def table_structure(table_name) - structure = exec_query("PRAGMA table_info(#{quote_table_name(table_name)})", 'SCHEMA').to_hash - raise(ActiveRecord::StatementInvalid, "Could not find table '#{table_name}'") if structure.empty? - structure - end - - def alter_table(table_name, options = {}) #:nodoc: - altered_table_name = "altered_#{table_name}" - caller = lambda {|definition| yield definition if block_given?} - - transaction do - move_table(table_name, altered_table_name, - options.merge(:temporary => true)) - move_table(altered_table_name, table_name, &caller) - end - end - - def move_table(from, to, options = {}, &block) #:nodoc: - copy_table(from, to, options, &block) - drop_table(from) - end - - def copy_table(from, to, options = {}) #:nodoc: - options = options.merge(:id => (!columns(from).detect{|c| c.name == 'id'}.nil? && 'id' == primary_key(from).to_s)) - create_table(to, options) do |definition| - @definition = definition - columns(from).each do |column| - column_name = options[:rename] ? - (options[:rename][column.name] || - options[:rename][column.name.to_sym] || - column.name) : column.name - - @definition.column(column_name, column.type, - :limit => column.limit, :default => column.default, - :precision => column.precision, :scale => column.scale, - :null => column.null) - end - @definition.primary_key(primary_key(from)) if primary_key(from) - yield @definition if block_given? - end - - copy_table_indexes(from, to, options[:rename] || {}) - copy_table_contents(from, to, - @definition.columns.map {|column| column.name}, - options[:rename] || {}) - end - - def copy_table_indexes(from, to, rename = {}) #:nodoc: - indexes(from).each do |index| - name = index.name - if to == "altered_#{from}" - name = "temp_#{name}" - elsif from == "altered_#{to}" - name = name[5..-1] - end - - 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 - - unless columns.empty? - # index name can't be the same - opts = { :name => name.gsub(/_(#{from})_/, "_#{to}_") } - opts[:unique] = true if index.unique - add_index(to, columns, opts) - end - end - end - - def copy_table_contents(from, to, columns, rename = {}) #:nodoc: - 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) - 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 << ')' - exec_query sql - end - end - - def sqlite_version - @sqlite_version ||= SQLiteAdapter::Version.new(select_value('select sqlite_version(*)')) - end - - def default_primary_key_type - if supports_autoincrement? - 'INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL' - else - 'INTEGER PRIMARY KEY NOT NULL' - end - end - - def translate_exception(exception, message) - case exception.message - when /column(s)? .* (is|are) not unique/ - RecordNotUnique.new(message, exception) - else - super - end - end - - end - end -end diff --git a/activerecord/lib/active_record/connection_handling.rb b/activerecord/lib/active_record/connection_handling.rb index d7746826a9..3531be05bf 100644 --- a/activerecord/lib/active_record/connection_handling.rb +++ b/activerecord/lib/active_record/connection_handling.rb @@ -1,4 +1,3 @@ -require 'active_support/core_ext/module/delegation' module ActiveRecord module ConnectionHandling @@ -45,7 +44,7 @@ module ActiveRecord end remove_connection - connection_handler.establish_connection name, spec + connection_handler.establish_connection self, spec end # Returns the connection currently associated with the class. This can @@ -90,7 +89,11 @@ module ActiveRecord connection_handler.remove_connection(klass) end + def clear_cache! # :nodoc: + connection.schema_cache.clear! + end + delegate :clear_active_connections!, :clear_reloadable_connections!, - :clear_all_connections!, :verify_active_connections!, :to => :connection_handler + :clear_all_connections!, :to => :connection_handler end end diff --git a/activerecord/lib/active_record/core.rb b/activerecord/lib/active_record/core.rb index 3f90d25962..f97c363871 100644 --- a/activerecord/lib/active_record/core.rb +++ b/activerecord/lib/active_record/core.rb @@ -1,87 +1,90 @@ -require 'active_support/concern' +require 'active_support/core_ext/hash/indifferent_access' +require 'active_support/core_ext/object/duplicable' require 'thread' module ActiveRecord + ActiveSupport.on_load(:active_record_config) do + ## + # :singleton-method: + # + # Accepts a logger conforming to the interface of Log4r 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+. + mattr_accessor :logger, instance_accessor: false + + ## + # :singleton-method: + # Contains the database configuration - as is typically stored in config/database.yml - + # as a Hash. + # + # For example, the following database.yml... + # + # development: + # adapter: sqlite3 + # database: db/development.sqlite3 + # + # production: + # adapter: sqlite3 + # database: db/production.sqlite3 + # + # ...would result in ActiveRecord::Base.configurations to look like this: + # + # { + # 'development' => { + # 'adapter' => 'sqlite3', + # 'database' => 'db/development.sqlite3' + # }, + # 'production' => { + # 'adapter' => 'sqlite3', + # 'database' => 'db/production.sqlite3' + # } + # } + mattr_accessor :configurations, instance_accessor: false + self.configurations = {} + + ## + # :singleton-method: + # Determines whether to use Time.utc (using :utc) or Time.local (using :local) when pulling + # dates and times from the database. This is set to :utc by default. + mattr_accessor :default_timezone, instance_accessor: false + self.default_timezone = :utc + + ## + # :singleton-method: + # Specifies the format to use when dumping the database schema with Rails' + # Rakefile. If :sql, the schema is dumped as (potentially database- + # specific) SQL statements. If :ruby, the schema is dumped as an + # ActiveRecord::Schema file which can be loaded into any database that + # supports migrations. Use :ruby if you want to have different database + # adapters for, e.g., your development and test environments. + mattr_accessor :schema_format, instance_accessor: false + self.schema_format = :ruby + + ## + # :singleton-method: + # Specify whether or not to use timestamps for migration versions + mattr_accessor :timestamped_migrations, instance_accessor: false + self.timestamped_migrations = true + + mattr_accessor :connection_handler, instance_accessor: false + self.connection_handler = ConnectionAdapters::ConnectionHandler.new + + mattr_accessor :dependent_restrict_raises, instance_accessor: false + self.dependent_restrict_raises = true + end + module Core extend ActiveSupport::Concern included do ## # :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 - # a class and instance level by calling +logger+. - config_attribute :logger, :global => true - - ## - # :singleton-method: - # Contains the database configuration - as is typically stored in config/database.yml - - # as a Hash. - # - # For example, the following database.yml... - # - # development: - # adapter: sqlite3 - # database: db/development.sqlite3 - # - # production: - # adapter: sqlite3 - # database: db/production.sqlite3 - # - # ...would result in ActiveRecord::Base.configurations to look like this: - # - # { - # 'development' => { - # 'adapter' => 'sqlite3', - # 'database' => 'db/development.sqlite3' - # }, - # 'production' => { - # 'adapter' => 'sqlite3', - # 'database' => 'db/production.sqlite3' - # } - # } - config_attribute :configurations, :global => true - self.configurations = {} - - ## - # :singleton-method: - # Determines whether to use Time.utc (using :utc) or Time.local (using :local) when pulling - # dates and times from the database. This is set to :utc by default. - config_attribute :default_timezone, :global => true - self.default_timezone = :utc - - ## - # :singleton-method: - # Specifies the format to use when dumping the database schema with Rails' - # Rakefile. If :sql, the schema is dumped as (potentially database- - # specific) SQL statements. If :ruby, the schema is dumped as an - # ActiveRecord::Schema file which can be loaded into any database that - # supports migrations. Use :ruby if you want to have different database - # adapters for, e.g., your development and test environments. - config_attribute :schema_format, :global => true - self.schema_format = :ruby - - ## - # :singleton-method: - # Specify whether or not to use timestamps for migration versions - config_attribute :timestamped_migrations, :global => true - self.timestamped_migrations = true - - ## - # :singleton-method: # The connection handler config_attribute :connection_handler - self.connection_handler = ConnectionAdapters::ConnectionHandler.new - ## - # :singleton-method: - # Specifies wether or not has_many or has_one association option - # :dependent => :restrict raises an exception. If set to true, the - # ActiveRecord::DeleteRestrictionError exception will be raised - # along with a DEPRECATION WARNING. If set to false, an error would - # be added to the model instead. - config_attribute :dependent_restrict_raises, :global => true - self.dependent_restrict_raises = true + %w(logger configurations default_timezone schema_format timestamped_migrations).each do |name| + config_attribute name, global: true + end end module ClassMethods @@ -125,23 +128,29 @@ module ActiveRecord object.is_a?(self) end + # Returns an instance of <tt>Arel::Table</tt> loaded with the current table name. + # + # class Post < ActiveRecord::Base + # scope :published_and_commented, published.and(self.arel_table[:comments_count].gt(0)) + # end def arel_table @arel_table ||= Arel::Table.new(table_name, arel_engine) end + # Returns the Arel engine. def arel_engine - @arel_engine ||= connection_handler.connection_pools[name] ? self : active_record_super.arel_engine + @arel_engine ||= connection_handler.retrieve_connection_pool(self) ? self : active_record_super.arel_engine end private def relation #:nodoc: - @relation ||= Relation.new(self, arel_table) + relation = Relation.new(self, arel_table) if finder_needs_type_condition? - @relation.where(type_condition).create_with(inheritance_column.to_sym => sti_name) + relation.where(type_condition).create_with(inheritance_column.to_sym => sti_name) else - @relation + relation end end end @@ -151,32 +160,24 @@ module ActiveRecord # In both instances, valid attribute keys are determined by the column names of the associated table -- # hence you can't have attributes that aren't part of the table columns. # - # +initialize+ respects mass-assignment security and accepts either +:as+ or +:without_protection+ options - # in the +options+ parameter. - # - # ==== Examples + # ==== Example: # # Instantiates a single new object # User.new(:first_name => 'Jamie') - # - # # Instantiates a single new object using the :admin mass-assignment security role - # User.new({ :first_name => 'Jamie', :is_admin => true }, :as => :admin) - # - # # Instantiates a single new object bypassing mass-assignment security - # User.new({ :first_name => 'Jamie', :is_admin => true }, :without_protection => true) - def initialize(attributes = nil, options = {}) - @attributes = self.class.initialize_attributes(self.class.column_defaults.dup) + def initialize(attributes = nil) + defaults = self.class.column_defaults.dup + defaults.each { |k, v| defaults[k] = v.dup if v.duplicable? } + + @attributes = self.class.initialize_attributes(defaults) @columns_hash = self.class.column_types.dup init_internals - ensure_proper_type - populate_with_current_scope_attributes - assign_attributes(attributes, options) if attributes + assign_attributes(attributes) if attributes yield self if block_given? - run_callbacks :initialize + run_callbacks :initialize unless _initialize_callbacks.empty? end # Initialize an empty model object from +coder+. +coder+ must contain @@ -190,10 +191,9 @@ module ActiveRecord # post.init_with('attributes' => { 'title' => 'hello world' }) # post.title # => 'hello world' def init_with(coder) - @attributes = self.class.initialize_attributes(coder['attributes']) + @attributes = self.class.initialize_attributes(coder['attributes']) @columns_hash = self.class.column_types.merge(coder['column_types'] || {}) - init_internals @new_record = false @@ -204,22 +204,41 @@ module ActiveRecord self end + ## + # :method: clone + # Identical to Ruby's clone method. This is a "shallow" copy. Be warned that your attributes are not copied. + # That means that modifying attributes of the clone will modify the original, since they will both point to the + # same attributes hash. If you need a copy of your attributes hash, please use the #dup method. + # + # user = User.first + # new_user = user.clone + # user.name # => "Bob" + # new_user.name = "Joe" + # user.name # => "Joe" + # + # user.object_id == new_user.object_id # => false + # user.name.object_id == new_user.name.object_id # => true + # + # user.name.object_id == user.dup.name.object_id # => false + + ## + # :method: dup # 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) - self.class.initialize_attributes(cloned_attributes) - cloned_attributes.delete(self.class.primary_key) + ## + def initialize_dup(other) # :nodoc: + cloned_attributes = other.clone_attributes(:read_attribute_before_type_cast) + self.class.initialize_attributes(cloned_attributes, :serialized => false) @attributes = cloned_attributes @attributes[self.class.primary_key] = nil - run_callbacks(:initialize) if _initialize_callbacks.any? + run_callbacks(:initialize) unless _initialize_callbacks.empty? @changed_attributes = {} self.class.column_defaults.each do |attr, orig_value| @@ -278,7 +297,8 @@ module ActiveRecord # Freeze the attributes hash such that associations are still accessible, even on destroyed records. def freeze - @attributes.freeze; self + @attributes.freeze + self end # Returns +true+ if the attributes hash has been frozen. @@ -290,8 +310,6 @@ module ActiveRecord def <=>(other_object) if other_object.is_a?(self.class) self.to_key <=> other_object.to_key - else - nil end end @@ -327,6 +345,11 @@ module ActiveRecord "#<#{self.class} #{inspection}>" end + # Returns a hash of the given methods with their names as keys and returned values as values. + def slice(*methods) + Hash[methods.map { |method| [method, public_send(method)] }].with_indifferent_access + end + private # Under Ruby 1.9, Array#flatten will call #to_ary (recursively) on each of the elements @@ -336,26 +359,26 @@ module ActiveRecord # # So we can avoid the method_missing hit by explicitly defining #to_ary as nil here. # - # See also http://tenderlovemaking.com/2011/06/28/til-its-ok-to-return-nil-from-to_ary/ + # See also http://tenderlovemaking.com/2011/06/28/til-its-ok-to-return-nil-from-to_ary.html def to_ary # :nodoc: nil end def init_internals pk = self.class.primary_key - @attributes[pk] = nil unless @attributes.key?(pk) - @relation = nil - @aggregation_cache = {} - @association_cache = {} - @attributes_cache = {} - @previously_changed = {} - @changed_attributes = {} - @readonly = false - @destroyed = false - @marked_for_destruction = false - @new_record = true + @aggregation_cache = {} + @association_cache = {} + @attributes_cache = {} + @previously_changed = {} + @changed_attributes = {} + @readonly = false + @destroyed = false + @marked_for_destruction = false + @new_record = true + @txn = nil + @_start_transaction_state = {} end end end diff --git a/activerecord/lib/active_record/counter_cache.rb b/activerecord/lib/active_record/counter_cache.rb index 224f5276eb..c877079b25 100644 --- a/activerecord/lib/active_record/counter_cache.rb +++ b/activerecord/lib/active_record/counter_cache.rb @@ -1,113 +1,115 @@ module ActiveRecord # = Active Record Counter Cache module CounterCache - # Resets one or more counter caches to their correct value using an SQL - # count query. This is useful when adding new counter caches, or if the - # counter has been corrupted or modified directly by SQL. - # - # ==== Parameters - # - # * +id+ - The id of the object you wish to reset a counter on. - # * +counters+ - One or more counter names to reset - # - # ==== Examples - # - # # For Post with id #1 records reset the comments_count - # Post.reset_counters(1, :comments) - def reset_counters(id, *counters) - object = find(id) - counters.each do |association| - has_many_association = reflect_on_association(association.to_sym) + extend ActiveSupport::Concern - foreign_key = has_many_association.foreign_key.to_s - child_class = has_many_association.klass - belongs_to = child_class.reflect_on_all_associations(:belongs_to) - reflection = belongs_to.find { |e| e.foreign_key.to_s == foreign_key } - counter_name = reflection.counter_cache_column + module ClassMethods + # Resets one or more counter caches to their correct value using an SQL + # count query. This is useful when adding new counter caches, or if the + # counter has been corrupted or modified directly by SQL. + # + # ==== Parameters + # + # * +id+ - The id of the object you wish to reset a counter on. + # * +counters+ - One or more counter names to reset + # + # ==== Examples + # + # # For Post with id #1 records reset the comments_count + # Post.reset_counters(1, :comments) + def reset_counters(id, *counters) + object = find(id) + counters.each do |association| + has_many_association = reflect_on_association(association.to_sym) - stmt = unscoped.where(arel_table[primary_key].eq(object.id)).arel.compile_update({ - arel_table[counter_name] => object.send(association).count - }) - connection.update stmt - end - return true - end + foreign_key = has_many_association.foreign_key.to_s + child_class = has_many_association.klass + belongs_to = child_class.reflect_on_all_associations(:belongs_to) + reflection = belongs_to.find { |e| e.foreign_key.to_s == foreign_key && e.options[:counter_cache].present? } + counter_name = reflection.counter_cache_column - # A generic "counter updater" implementation, intended primarily to be - # used by increment_counter and decrement_counter, but which may also - # be useful on its own. It simply does a direct SQL update for the record - # with the given ID, altering the given hash of counters by the amount - # given by the corresponding value: - # - # ==== Parameters - # - # * +id+ - The id of the object you wish to update a counter on or an Array of ids. - # * +counters+ - An Array of Hashes containing the names of the fields - # to update as keys and the amount to update the field by as values. - # - # ==== Examples - # - # # For the Post with id of 5, decrement the comment_count by 1, and - # # increment the action_count by 1 - # Post.update_counters 5, :comment_count => -1, :action_count => 1 - # # Executes the following SQL: - # # UPDATE posts - # # 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 = COALESCE(comment_count, 0) + 1 - # # WHERE id IN (10, 15) - def update_counters(id, counters) - updates = counters.map do |counter_name, value| - operator = value < 0 ? '-' : '+' - quoted_column = connection.quote_column_name(counter_name) - "#{quoted_column} = COALESCE(#{quoted_column}, 0) #{operator} #{value.abs}" + stmt = unscoped.where(arel_table[primary_key].eq(object.id)).arel.compile_update({ + arel_table[counter_name] => object.send(association).count + }) + connection.update stmt + end + return true end - IdentityMap.remove_by_id(symbolized_base_class, id) if IdentityMap.enabled? + # A generic "counter updater" implementation, intended primarily to be + # used by increment_counter and decrement_counter, but which may also + # be useful on its own. It simply does a direct SQL update for the record + # with the given ID, altering the given hash of counters by the amount + # given by the corresponding value: + # + # ==== Parameters + # + # * +id+ - The id of the object you wish to update a counter on or an Array of ids. + # * +counters+ - An Array of Hashes containing the names of the fields + # to update as keys and the amount to update the field by as values. + # + # ==== Examples + # + # # For the Post with id of 5, decrement the comment_count by 1, and + # # increment the action_count by 1 + # Post.update_counters 5, :comment_count => -1, :action_count => 1 + # # Executes the following SQL: + # # UPDATE posts + # # 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 = COALESCE(comment_count, 0) + 1 + # # WHERE id IN (10, 15) + def update_counters(id, counters) + updates = counters.map do |counter_name, value| + operator = value < 0 ? '-' : '+' + quoted_column = connection.quote_column_name(counter_name) + "#{quoted_column} = COALESCE(#{quoted_column}, 0) #{operator} #{value.abs}" + end - update_all(updates.join(', '), primary_key => id ) - end + where(primary_key => id).update_all updates.join(', ') + end - # Increment a number field by one, usually representing a count. - # - # This is used for caching aggregate values, so that they don't need to be computed every time. - # For example, a DiscussionBoard may cache post_count and comment_count otherwise every time the board is - # shown it would have to run an SQL query to find how many posts and comments there are. - # - # ==== Parameters - # - # * +counter_name+ - The name of the field that should be incremented. - # * +id+ - The id of the object that should be incremented. - # - # ==== Examples - # - # # Increment the post_count column for the record with an id of 5 - # DiscussionBoard.increment_counter(:post_count, 5) - def increment_counter(counter_name, id) - update_counters(id, counter_name => 1) - end + # Increment a number field by one, usually representing a count. + # + # This is used for caching aggregate values, so that they don't need to be computed every time. + # For example, a DiscussionBoard may cache post_count and comment_count otherwise every time the board is + # shown it would have to run an SQL query to find how many posts and comments there are. + # + # ==== Parameters + # + # * +counter_name+ - The name of the field that should be incremented. + # * +id+ - The id of the object that should be incremented. + # + # ==== Examples + # + # # Increment the post_count column for the record with an id of 5 + # DiscussionBoard.increment_counter(:post_count, 5) + def increment_counter(counter_name, id) + update_counters(id, counter_name => 1) + end - # Decrement a number field by one, usually representing a count. - # - # This works the same as increment_counter but reduces the column value by 1 instead of increasing it. - # - # ==== Parameters - # - # * +counter_name+ - The name of the field that should be decremented. - # * +id+ - The id of the object that should be decremented. - # - # ==== Examples - # - # # Decrement the post_count column for the record with an id of 5 - # DiscussionBoard.decrement_counter(:post_count, 5) - def decrement_counter(counter_name, id) - update_counters(id, counter_name => -1) + # Decrement a number field by one, usually representing a count. + # + # This works the same as increment_counter but reduces the column value by 1 instead of increasing it. + # + # ==== Parameters + # + # * +counter_name+ - The name of the field that should be decremented. + # * +id+ - The id of the object that should be decremented. + # + # ==== Examples + # + # # Decrement the post_count column for the record with an id of 5 + # DiscussionBoard.decrement_counter(:post_count, 5) + def decrement_counter(counter_name, id) + update_counters(id, counter_name => -1) + end end end end diff --git a/activerecord/lib/active_record/dynamic_finder_match.rb b/activerecord/lib/active_record/dynamic_finder_match.rb deleted file mode 100644 index 38dbbef5fc..0000000000 --- a/activerecord/lib/active_record/dynamic_finder_match.rb +++ /dev/null @@ -1,84 +0,0 @@ -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) - method = method.to_s - klass = [FindBy, FindByBang, FindOrInitializeCreateBy].find do |_klass| - _klass.matches?(method) - end - klass.new(method) if klass - end - - def self.matches?(method) - method =~ self::METHOD_PATTERN - end - - def initialize(method) - @finder = :first - @instantiator = nil - match_data = method.match(self.class::METHOD_PATTERN) - @attribute_names = match_data[-1].split("_and_") - initialize_from_match_data(match_data) - end - - attr_reader :finder, :attribute_names, :instantiator - - def finder? - @finder && !@instantiator - end - - def creator? - @finder == :first && @instantiator == :create - end - - def instantiator? - @instantiator - end - - def bang? - false - end - - def valid_arguments?(arguments) - arguments.size >= @attribute_names.size - end - - private - - def initialize_from_match_data(match_data) - end - end - - class FindBy < DynamicFinderMatch - METHOD_PATTERN = /^find_(all_|last_)?by_([_a-zA-Z]\w*)$/ - - def initialize_from_match_data(match_data) - @finder = :last if match_data[1] == 'last_' - @finder = :all if match_data[1] == 'all_' - end - end - - class FindByBang < DynamicFinderMatch - METHOD_PATTERN = /^find_by_([_a-zA-Z]\w*)\!$/ - - def bang? - true - end - end - - class FindOrInitializeCreateBy < DynamicFinderMatch - METHOD_PATTERN = /^find_or_(initialize|create)_by_([_a-zA-Z]\w*)$/ - - def initialize_from_match_data(match_data) - @instantiator = match_data[1] == 'initialize' ? :new : :create - end - - def valid_arguments?(arguments) - arguments.size == 1 && arguments.first.is_a?(Hash) || super - end - end -end diff --git a/activerecord/lib/active_record/dynamic_matchers.rb b/activerecord/lib/active_record/dynamic_matchers.rb index 60ce3dd4f1..3bac31c6aa 100644 --- a/activerecord/lib/active_record/dynamic_matchers.rb +++ b/activerecord/lib/active_record/dynamic_matchers.rb @@ -1,79 +1,131 @@ module ActiveRecord - module DynamicMatchers - def respond_to?(method_id, include_private = false) - if match = DynamicFinderMatch.match(method_id) - return true if all_attributes_exists?(match.attribute_names) - elsif match = DynamicScopeMatch.match(method_id) - return true if all_attributes_exists?(match.attribute_names) - end + module DynamicMatchers #:nodoc: + # This code in this file seems to have a lot of indirection, but the indirection + # is there to provide extension points for the activerecord-deprecated_finders + # gem. When we stop supporting activerecord-deprecated_finders (from Rails 5), + # then we can remove the indirection. - super + def respond_to?(name, include_private = false) + match = Method.match(self, name) + match && match.valid? || super end private - # 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 - # 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 - # 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) || DynamicScopeMatch.match(method_id)) - attribute_names = match.attribute_names - super unless all_attributes_exists?(attribute_names) - unless match.valid_arguments?(arguments) - method_trace = "#{__FILE__}:#{__LINE__}:in `#{method_id}'" - backtrace = [method_trace] + caller - raise ArgumentError, "wrong number of arguments (#{arguments.size} for #{attribute_names.size})", backtrace - end - if match.respond_to?(:scope?) && match.scope? - self.class_eval <<-METHOD, __FILE__, __LINE__ + 1 - 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) - elsif match.finder? - options = arguments.extract_options! - relation = options.any? ? scoped(options) : scoped - relation.send :find_by_attributes, match, attribute_names, *arguments, &block - elsif match.instantiator? - scoped.send :find_or_instantiator_by_attributes, match, attribute_names, *arguments, &block - end + def method_missing(name, *arguments, &block) + match = Method.match(self, name) + + if match && match.valid? + match.define + send(name, *arguments, &block) else super end end - # Similar in purpose to +expand_hash_conditions_for_aggregates+. - def expand_attribute_names_for_aggregates(attribute_names) - attribute_names.map { |attribute_name| - unless (aggregation = reflect_on_aggregation(attribute_name.to_sym)).nil? - aggregate_mapping(aggregation).map do |field_attr, _| - field_attr.to_sym - end - else - attribute_name.to_sym + class Method + @matchers = [] + + class << self + attr_reader :matchers + + def match(model, name) + klass = matchers.find { |k| name =~ k.pattern } + klass.new(model, name) if klass + end + + def pattern + /^#{prefix}_([_a-zA-Z]\w*)#{suffix}$/ + end + + def prefix + raise NotImplementedError end - }.flatten + + def suffix + '' + end + end + + attr_reader :model, :name, :attribute_names + + def initialize(model, name) + @model = model + @name = name.to_s + @attribute_names = @name.match(self.class.pattern)[1].split('_and_') + @attribute_names.map! { |n| @model.attribute_aliases[n] || n } + end + + def valid? + attribute_names.all? { |name| model.columns_hash[name] || model.reflect_on_aggregation(name.to_sym) } + end + + def define + model.class_eval <<-CODE, __FILE__, __LINE__ + 1 + def self.#{name}(#{signature}) + #{body} + end + CODE + end + + def body + raise NotImplementedError + end end - def all_attributes_exists?(attribute_names) - (expand_attribute_names_for_aggregates(attribute_names) - - column_methods_hash.keys).empty? + module Finder + # Extended in activerecord-deprecated_finders + def body + result + end + + # Extended in activerecord-deprecated_finders + def result + "#{finder}(#{attributes_hash})" + end + + # Extended in activerecord-deprecated_finders + def signature + attribute_names.join(', ') + end + + def attributes_hash + "{" + attribute_names.map { |name| ":#{name} => #{name}" }.join(',') + "}" + end + + def finder + raise NotImplementedError + end end - def aggregate_mapping(reflection) - mapping = reflection.options[:mapping] || [reflection.name, reflection.name] - mapping.first.is_a?(Array) ? mapping : [mapping] + class FindBy < Method + Method.matchers << self + include Finder + + def self.prefix + "find_by" + end + + def finder + "find_by" + end end + class FindByBang < Method + Method.matchers << self + include Finder + + def self.prefix + "find_by" + end + + def self.suffix + "!" + end + def finder + "find_by!" + end + end end end diff --git a/activerecord/lib/active_record/dynamic_scope_match.rb b/activerecord/lib/active_record/dynamic_scope_match.rb deleted file mode 100644 index a502155aac..0000000000 --- a/activerecord/lib/active_record/dynamic_scope_match.rb +++ /dev/null @@ -1,27 +0,0 @@ -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) - return unless method.to_s =~ /^scoped_by_([_a-zA-Z]\w*)$/ - new(true, $1 && $1.split('_and_')) - end - - def initialize(scope, attribute_names) - @scope = scope - @attribute_names = attribute_names - end - - attr_reader :scope, :attribute_names - alias :scope? :scope - - def valid_arguments?(arguments) - arguments.size >= @attribute_names.size - end - end -end diff --git a/activerecord/lib/active_record/errors.rb b/activerecord/lib/active_record/errors.rb index fc80f3081e..5f157fde6d 100644 --- a/activerecord/lib/active_record/errors.rb +++ b/activerecord/lib/active_record/errors.rb @@ -53,6 +53,10 @@ module ActiveRecord class RecordNotSaved < ActiveRecordError end + # Raised by ActiveRecord::Base.destroy! when a call to destroy would return false. + class RecordNotDestroyed < ActiveRecordError + end + # 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 @@ -102,13 +106,11 @@ module ActiveRecord attr_reader :record, :attempted_action def initialize(record, attempted_action) + super("Attempted to #{attempted_action} a stale object: #{record.class.name}") @record = record @attempted_action = attempted_action end - def message - "Attempted to #{attempted_action} a stale object: #{record.class.name}" - end end # Raised when association is being configured improperly or @@ -164,9 +166,9 @@ module ActiveRecord class AttributeAssignmentError < ActiveRecordError attr_reader :exception, :attribute def initialize(message, exception, attribute) + super(message) @exception = exception @attribute = attribute - @message = message end end @@ -185,11 +187,12 @@ module ActiveRecord attr_reader :model def initialize(model) + super("Unknown primary key for table #{model.table_name} in model #{model}.") @model = model end - def message - "Unknown primary key for table #{model.table_name} in model #{model}." - end + end + + class ImmutableRelation < ActiveRecordError end end diff --git a/activerecord/lib/active_record/explain.rb b/activerecord/lib/active_record/explain.rb index 01cacf6153..9e0390bed1 100644 --- a/activerecord/lib/active_record/explain.rb +++ b/activerecord/lib/active_record/explain.rb @@ -1,12 +1,12 @@ -require 'active_support/core_ext/class/attribute' +require 'active_support/lazy_load_hooks' module ActiveRecord + ActiveSupport.on_load(:active_record_config) do + mattr_accessor :auto_explain_threshold_in_seconds, instance_accessor: false + end + module Explain - def self.extended(base) - # If a query takes longer than these many seconds we log its query plan - # automatically. nil disables this feature. - base.config_attribute :auto_explain_threshold_in_seconds, :global => true - end + delegate :auto_explain_threshold_in_seconds, :auto_explain_threshold_in_seconds=, to: 'ActiveRecord::Model' # If auto explain is enabled, this method triggers EXPLAIN logging for the # queries triggered by the block if it takes more than the threshold as a @@ -52,7 +52,7 @@ module ActiveRecord # Makes the adapter execute EXPLAIN for the tuples of queries and bindings. # Returns a formatted string ready to be logged. def exec_explain(queries) # :nodoc: - queries && queries.map do |sql, bind| + str = queries && queries.map do |sql, bind| [].tap do |msg| msg << "EXPLAIN for: #{sql}" unless bind.empty? @@ -62,6 +62,12 @@ module ActiveRecord msg << connection.explain(sql, bind) end.join("\n") end.join("\n") + + # Overriding inspect to be more human readable, specially in the console. + def str.inspect + self + end + str end # Silences automatic EXPLAIN logging for the duration of the block. @@ -70,7 +76,7 @@ module ActiveRecord # the threshold is set to 0. # # As the name of the method suggests this only applies to automatic - # EXPLAINs, manual calls to +ActiveRecord::Relation#explain+ run. + # EXPLAINs, manual calls to <tt>ActiveRecord::Relation#explain</tt> run. def silence_auto_explain current = Thread.current original, current[:available_queries_for_explain] = current[:available_queries_for_explain], false diff --git a/activerecord/lib/active_record/explain_subscriber.rb b/activerecord/lib/active_record/explain_subscriber.rb index 1f8c4fc203..0f927496fb 100644 --- a/activerecord/lib/active_record/explain_subscriber.rb +++ b/activerecord/lib/active_record/explain_subscriber.rb @@ -2,9 +2,12 @@ require 'active_support/notifications' module ActiveRecord class ExplainSubscriber # :nodoc: - def call(*args) + def start(name, id, payload) + # unused + end + + def finish(name, id, payload) if queries = Thread.current[:available_queries_for_explain] - payload = args.last queries << payload.values_at(:sql, :binds) unless ignore_payload?(payload) end end @@ -15,8 +18,9 @@ module ActiveRecord # On the other hand, we want to monitor the performance of our real database # queries, not the performance of the access to the query cache. IGNORED_PAYLOADS = %w(SCHEMA EXPLAIN CACHE) + EXPLAINED_SQLS = /\A\s*(select|update|delete|insert)/i def ignore_payload?(payload) - payload[:exception] || IGNORED_PAYLOADS.include?(payload[:name]) + payload[:exception] || IGNORED_PAYLOADS.include?(payload[:name]) || payload[:sql] !~ EXPLAINED_SQLS end ActiveSupport::Notifications.subscribe("sql.active_record", new) diff --git a/activerecord/lib/active_record/fixtures.rb b/activerecord/lib/active_record/fixtures.rb index b82d5b5621..60fc653735 100644 --- a/activerecord/lib/active_record/fixtures.rb +++ b/activerecord/lib/active_record/fixtures.rb @@ -2,18 +2,13 @@ require 'erb' require 'yaml' require 'zlib' require 'active_support/dependencies' -require 'active_support/core_ext/object/blank' require 'active_record/fixtures/file' +require 'active_record/errors' -if defined? ActiveRecord +module ActiveRecord class FixtureClassNotFound < ActiveRecord::ActiveRecordError #:nodoc: end -else - class FixtureClassNotFound < StandardError #:nodoc: - end -end -module ActiveRecord # \Fixtures are a way of organizing data that you want to test against; in short, sample data. # # They are stored in YAML files, one file per model, which are placed in the directory @@ -87,7 +82,7 @@ module ActiveRecord # end # # test "find_alt_method_2" do - # assert_equal "Ruby on Rails", @rubyonrails.news + # assert_equal "Ruby on Rails", @rubyonrails.name # end # # In order to use these methods to access fixtured data within your testcases, you must specify one of the @@ -376,19 +371,23 @@ module ActiveRecord # # Any fixture labeled "DEFAULTS" is safely ignored. class Fixtures + #-- + # NOTE: an instance of Fixtures can be called fixture_set, it is normally stored in a single YAML file and possibly in a folder with the same name. + #++ MAX_ID = 2 ** 30 - 1 @@all_cached_fixtures = Hash.new { |h,k| h[k] = {} } - def self.default_fixture_model_name(fixture_name) # :nodoc: + def self.default_fixture_model_name(fixture_set_name) # :nodoc: ActiveRecord::Base.pluralize_table_names ? - fixture_name.singularize.camelize : - fixture_name.camelize + fixture_set_name.singularize.camelize : + fixture_set_name.camelize end - def self.default_fixture_table_name(fixture_name) # :nodoc: - "#{ActiveRecord::Base.table_name_prefix}"\ - "#{fixture_name.tr('/', '_')}#{ActiveRecord::Base.table_name_suffix}".to_sym + def self.default_fixture_table_name(fixture_set_name) # :nodoc: + "#{ ActiveRecord::Base.table_name_prefix }"\ + "#{ fixture_set_name.tr('/', '_') }"\ + "#{ ActiveRecord::Base.table_name_suffix }".to_sym end def self.reset_cache @@ -415,11 +414,11 @@ module ActiveRecord cache_for_connection(connection).update(fixtures_map) end - def self.instantiate_fixtures(object, fixture_name, fixtures, load_instances = true) + def self.instantiate_fixtures(object, fixture_set, load_instances = true) if load_instances - fixtures.each do |name, fixture| + fixture_set.each do |fixture_name, fixture| begin - object.instance_variable_set "@#{name}", fixture.find + object.instance_variable_set "@#{fixture_name}", fixture.find rescue FixtureClassNotFound nil end @@ -428,61 +427,59 @@ module ActiveRecord end def self.instantiate_all_loaded_fixtures(object, load_instances = true) - all_loaded_fixtures.each do |table_name, fixtures| - ActiveRecord::Fixtures.instantiate_fixtures(object, table_name, fixtures, load_instances) + all_loaded_fixtures.each_value do |fixture_set| + instantiate_fixtures(object, fixture_set, load_instances) end end cattr_accessor :all_loaded_fixtures self.all_loaded_fixtures = {} - def self.create_fixtures(fixtures_directory, table_names, class_names = {}) - table_names = Array(table_names).map(&:to_s) + def self.create_fixtures(fixtures_directory, fixture_set_names, class_names = {}) + fixture_set_names = Array(fixture_set_names).map(&:to_s) class_names = class_names.stringify_keys # FIXME: Apparently JK uses this. connection = block_given? ? yield : ActiveRecord::Base.connection - files_to_read = table_names.reject { |table_name| - fixture_is_cached?(connection, table_name) + files_to_read = fixture_set_names.reject { |fs_name| + fixture_is_cached?(connection, fs_name) } unless files_to_read.empty? connection.disable_referential_integrity do fixtures_map = {} - fixture_files = files_to_read.map do |path| - fixture_name = path - - fixtures_map[fixture_name] = new( # ActiveRecord::Fixtures.new + fixture_sets = files_to_read.map do |fs_name| + fixtures_map[fs_name] = new( # ActiveRecord::Fixtures.new connection, - fixture_name, - class_names[fixture_name.to_s] || default_fixture_model_name(fixture_name), - ::File.join(fixtures_directory, path)) + fs_name, + class_names[fs_name] || default_fixture_model_name(fs_name), + ::File.join(fixtures_directory, fs_name)) end 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 + fixture_sets.each do |fs| + conn = fs.model_class.respond_to?(:connection) ? fs.model_class.connection : connection + table_rows = fs.table_rows table_rows.keys.each do |table| conn.delete "DELETE FROM #{conn.quote_table_name(table)}", 'Fixture Delete' end - table_rows.each do |table_name,rows| + table_rows.each do |fixture_set_name, rows| rows.each do |row| - conn.insert_fixture(row, table_name) + conn.insert_fixture(row, fixture_set_name) end end end # Cap primary key sequences to max(pk). if connection.respond_to?(:reset_pk_sequence!) - fixture_files.each do |ff| - connection.reset_pk_sequence!(ff.table_name) + fixture_sets.each do |fs| + connection.reset_pk_sequence!(fs.table_name) end end end @@ -490,7 +487,7 @@ module ActiveRecord cache_fixtures(connection, fixtures_map) end end - cached_fixtures(connection, table_names) + cached_fixtures(connection, fixture_set_names) end # Returns a consistent, platform-independent identifier for +label+. @@ -501,26 +498,23 @@ module ActiveRecord attr_reader :table_name, :name, :fixtures, :model_class - def initialize(connection, fixture_name, class_name, fixture_path) - @connection = connection - @fixture_path = fixture_path - @name = fixture_name - @class_name = class_name - - @fixtures = {} + def initialize(connection, name, class_name, path) + @fixtures = {} # Ordered hash + @name = name + @path = path - # Should be an AR::Base type class - if class_name.is_a?(Class) + if class_name.is_a?(Class) # TODO: Should be an AR::Base type class, or any? @model_class = class_name else @model_class = class_name.constantize rescue nil end - @connection = model_class.connection if model_class && model_class.respond_to?(:connection) + @connection = ( model_class.respond_to?(:connection) ? + model_class.connection : connection ) @table_name = ( model_class.respond_to?(:table_name) ? model_class.table_name : - self.class.default_fixture_table_name(fixture_name) ) + self.class.default_fixture_table_name(name) ) read_fixture_files end @@ -559,8 +553,8 @@ module ActiveRecord if model_class && model_class < ActiveRecord::Model # fill in timestamp columns if they aren't specified and the model is set to record_timestamps if model_class.record_timestamps - timestamp_column_names.each do |name| - row[name] = now unless row.key?(name) + timestamp_column_names.each do |c_name| + row[c_name] = now unless row.key?(c_name) end end @@ -599,7 +593,7 @@ module ActiveRecord when :has_and_belongs_to_many if (targets = row.delete(association.name.to_s)) targets = targets.is_a?(Array) ? targets : targets.split(/\s*,\s*/) - table_name = association.options[:join_table] + table_name = association.join_table rows[table_name].concat targets.map { |target| { association.foreign_key => row[primary_key_name], association.association_foreign_key => ActiveRecord::Fixtures.identify(target) } @@ -638,26 +632,23 @@ module ActiveRecord end def read_fixture_files - yaml_files = Dir["#{@fixture_path}/**/*.yml"].select { |f| + yaml_files = Dir["#{@path}/**/*.yml"].select { |f| ::File.file?(f) } + [yaml_file_path] yaml_files.each do |file| Fixtures::File.open(file) do |fh| - fh.each do |name, row| - fixtures[name] = ActiveRecord::Fixture.new(row, model_class) + fh.each do |fixture_name, row| + fixtures[fixture_name] = ActiveRecord::Fixture.new(row, model_class) end end end end def yaml_file_path - "#{@fixture_path}.yml" + "#{@path}.yml" end - def yaml_fixtures_key(path) - ::File.basename(@fixture_path).split(".").first - end end class Fixture #:nodoc: @@ -712,7 +703,7 @@ module ActiveRecord 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 :use_instantiated_fixtures # true, false, or :no_instances class_attribute :pre_loaded_fixtures self.fixture_table_names = [] @@ -720,9 +711,8 @@ module ActiveRecord self.use_instantiated_fixtures = false self.pre_loaded_fixtures = false - self.fixture_class_names = Hash.new do |h, fixture_name| - fixture_name = fixture_name.to_s - h[fixture_name] = ActiveRecord::Fixtures.default_fixture_model_name(fixture_name) + self.fixture_class_names = Hash.new do |h, fixture_set_name| + h[fixture_set_name] = ActiveRecord::Fixtures.default_fixture_model_name(fixture_set_name) end end @@ -746,18 +736,18 @@ module ActiveRecord self.fixture_class_names = self.fixture_class_names.merge(class_names.stringify_keys) end - def fixtures(*fixture_names) - if fixture_names.first == :all - fixture_names = Dir["#{fixture_path}/**/*.yml"].map { |f| + def fixtures(*fixture_set_names) + if fixture_set_names.first == :all + fixture_set_names = Dir["#{fixture_path}/**/*.yml"].map { |f| File.basename f, '.yml' } else - fixture_names = fixture_names.flatten.map { |n| n.to_s } + fixture_set_names = fixture_set_names.flatten.map { |n| n.to_s } end - self.fixture_table_names |= fixture_names - require_fixture_classes(fixture_names) - setup_fixture_accessors(fixture_names) + self.fixture_table_names |= fixture_set_names + require_fixture_classes(fixture_set_names) + setup_fixture_accessors(fixture_set_names) end def try_to_load_dependency(file_name) @@ -772,35 +762,39 @@ module ActiveRecord end end - def require_fixture_classes(fixture_names = nil) - (fixture_names || fixture_table_names).each do |fixture_name| - file_name = fixture_name.to_s + def require_fixture_classes(fixture_set_names = nil) + if fixture_set_names + fixture_set_names = fixture_set_names.map { |n| n.to_s } + else + fixture_set_names = fixture_table_names + end + + fixture_set_names.each do |file_name| file_name = file_name.singularize if ActiveRecord::Base.pluralize_table_names try_to_load_dependency(file_name) end end - def setup_fixture_accessors(fixture_names = nil) - fixture_names = Array(fixture_names || fixture_table_names) + def setup_fixture_accessors(fixture_set_names = nil) + fixture_set_names = Array(fixture_set_names || fixture_table_names) methods = Module.new do - fixture_names.each do |fixture_name| - fixture_name = fixture_name.to_s - accessor_name = fixture_name.tr('/', '_').to_sym + fixture_set_names.each do |fs_name| + fs_name = fs_name.to_s + accessor_name = fs_name.tr('/', '_').to_sym - define_method(accessor_name) do |*fixtures| - force_reload = fixtures.pop if fixtures.last == true || fixtures.last == :reload + define_method(accessor_name) do |*fixture_names| + force_reload = fixture_names.pop if fixture_names.last == true || fixture_names.last == :reload - @fixture_cache[fixture_name] ||= {} + @fixture_cache[fs_name] ||= {} - instances = fixtures.map do |fixture| - @fixture_cache[fixture_name].delete(fixture) if force_reload + instances = fixture_names.map do |f_name| + f_name = f_name.to_s + @fixture_cache[fs_name].delete(f_name) if force_reload - 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 + if @loaded_fixtures[fs_name][f_name] + @fixture_cache[fs_name][f_name] ||= @loaded_fixtures[fs_name][f_name].find else - raise StandardError, "No entry named '#{fixture}' found for fixture collection '#{fixture_name}'" + raise StandardError, "No fixture named '#{f_name}' found for fixture set '#{fs_name}'" end end @@ -829,7 +823,7 @@ module ActiveRecord end def setup_fixtures - return unless !ActiveRecord::Base.configurations.blank? + return if ActiveRecord::Base.configurations.blank? if pre_loaded_fixtures && !use_transactional_fixtures raise RuntimeError, 'pre_loaded_fixtures requires use_transactional_fixtures' @@ -849,9 +843,7 @@ module ActiveRecord end @fixture_connections = enlist_fixture_connections @fixture_connections.each do |connection| - connection.increment_open_transactions - connection.transaction_joinable = false - connection.begin_db_transaction + connection.begin_transaction joinable: false end # Load fixtures for every test. else @@ -874,10 +866,7 @@ module ActiveRecord # Rollback changes if a transaction is active. if run_in_transaction? @fixture_connections.each do |connection| - if connection.open_transactions != 0 - connection.rollback_db_transaction - connection.decrement_open_transactions - end + connection.rollback_transaction if connection.transaction_open? end @fixture_connections.clear end @@ -885,7 +874,7 @@ module ActiveRecord end def enlist_fixture_connections - ActiveRecord::Base.connection_handler.connection_pools.values.map(&:connection) + ActiveRecord::Base.connection_handler.connection_pools.map(&:connection) end private @@ -907,8 +896,8 @@ module ActiveRecord 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 |fixture_name, fixtures| - ActiveRecord::Fixtures.instantiate_fixtures(self, fixture_name, fixtures, load_instances?) + @loaded_fixtures.each_value do |fixture_set| + ActiveRecord::Fixtures.instantiate_fixtures(self, fixture_set, load_instances?) end end end diff --git a/activerecord/lib/active_record/fixtures/file.rb b/activerecord/lib/active_record/fixtures/file.rb index 6547791144..a9cabf5a7b 100644 --- a/activerecord/lib/active_record/fixtures/file.rb +++ b/activerecord/lib/active_record/fixtures/file.rb @@ -24,37 +24,33 @@ module ActiveRecord rows.each(&block) end - RESCUE_ERRORS = [ ArgumentError ] # :nodoc: + RESCUE_ERRORS = [ ArgumentError, Psych::SyntaxError ] # :nodoc: private - if defined?(Psych) && defined?(Psych::SyntaxError) - RESCUE_ERRORS << Psych::SyntaxError - end - - def rows - return @rows if @rows + def rows + return @rows if @rows + + begin + data = YAML.load(render(IO.read(@file))) + rescue *RESCUE_ERRORS => error + raise Fixture::FormatError, "a YAML error occurred parsing #{@file}. Please note that YAML must be consistently indented using spaces. Tabs are not allowed. Please have a look at http://www.yaml.org/faq.html\nThe exact error was:\n #{error.class}: #{error}", error.backtrace + end + @rows = data ? validate(data).to_a : [] + end - begin - data = YAML.load(render(IO.read(@file))) - rescue *RESCUE_ERRORS => error - raise Fixture::FormatError, "a YAML error occurred parsing #{@file}. Please note that YAML must be consistently indented using spaces. Tabs are not allowed. Please have a look at http://www.yaml.org/faq.html\nThe exact error was:\n #{error.class}: #{error}", error.backtrace + def render(content) + ERB.new(content).result end - @rows = data ? validate(data).to_a : [] - end - def render(content) - ERB.new(content).result - end + # Validate our unmarshalled data. + def validate(data) + unless Hash === data || YAML::Omap === data + raise Fixture::FormatError, 'fixture is not a hash' + end - # Validate our unmarshalled data. - def validate(data) - unless Hash === data || YAML::Omap === data - raise Fixture::FormatError, 'fixture is not a hash' + raise Fixture::FormatError unless data.all? { |name, row| Hash === row } + data end - - raise Fixture::FormatError unless data.all? { |name, row| Hash === row } - data - end end end end diff --git a/activerecord/lib/active_record/identity_map.rb b/activerecord/lib/active_record/identity_map.rb deleted file mode 100644 index d9777bb2f6..0000000000 --- a/activerecord/lib/active_record/identity_map.rb +++ /dev/null @@ -1,144 +0,0 @@ -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 and still in development (i.e. use it with care). - # - # == Associations - # - # Active Record Identity Map does not track associations yet. For example: - # - # comment = @post.comments.first - # comment.post = nil - # @post.comments.include?(comment) #=> true - # - # Ideally, the example above would return false, removing the comment object from the - # post association when the association is nullified. This may cause side effects, as - # in the situation below, if Identity Map is enabled: - # - # Post.has_many :comments, :dependent => :destroy - # - # comment = @post.comments.first - # comment.post = nil - # comment.save - # Post.destroy(@post.id) - # - # Without using Identity Map, the code above will destroy the @post object leaving - # the comment object intact. However, once we enable Identity Map, the post loaded - # by Post.destroy is exactly the same object as the object @post. As the object @post - # still has the comment object in @post.comments, once Identity Map is enabled, the - # comment object will be accidently removed. - # - # This inconsistency is meant to be fixed in future Rails releases. - # - module IdentityMap - - 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) - record = repository[klass.symbolized_sti_name][primary_key] - - if record.is_a?(klass) - ActiveSupport::Notifications.instrument("identity.active_record", - :line => "From Identity Map (id: #{primary_key})", - :name => "#{klass} Loaded", - :connection_id => object_id) - - record - else - nil - end - end - - def add(record) - repository[record.class.symbolized_sti_name][record.id] = record - end - - def remove(record) - repository[record.class.symbolized_sti_name].delete(record.id) - end - - def remove_by_id(symbolized_sti_name, id) - repository[symbolized_sti_name].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 = self.class.initialize_attributes(coder['attributes'].except(*dirty)) - @attributes.update(attributes) - @changed_attributes.update(coder['attributes'].slice(*dirty)) - @changed_attributes.delete_if{|k,v| v.eql? @attributes[k]} - - run_callbacks :find - - self - end - - class Middleware - def initialize(app) - @app = app - end - - def call(env) - enabled = IdentityMap.enabled - IdentityMap.enabled = true - - response = @app.call(env) - response[2] = Rack::BodyProxy.new(response[2]) do - IdentityMap.enabled = enabled - IdentityMap.clear - end - - response - end - end - end -end diff --git a/activerecord/lib/active_record/inheritance.rb b/activerecord/lib/active_record/inheritance.rb index 2c766411a0..35273b0d81 100644 --- a/activerecord/lib/active_record/inheritance.rb +++ b/activerecord/lib/active_record/inheritance.rb @@ -1,13 +1,16 @@ -require 'active_support/concern' module ActiveRecord + ActiveSupport.on_load(:active_record_config) do + # Determine whether to store the full constant name including namespace when using STI + mattr_accessor :store_full_sti_class, instance_accessor: false + self.store_full_sti_class = true + end + module Inheritance extend ActiveSupport::Concern included do - # Determine whether to store the full constant name including namespace when using STI config_attribute :store_full_sti_class - self.store_full_sti_class = true end module ClassMethods @@ -37,17 +40,43 @@ module ActiveRecord @symbolized_sti_name ||= sti_name.present? ? sti_name.to_sym : symbolized_base_class 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 + # Returns the class descending directly from ActiveRecord::Base (or + # that includes ActiveRecord::Model), or an abstract class, if any, in + # the inheritance hierarchy. + # + # 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. # # If B < A and C < B and if A is an abstract_class then both B.base_class # and C.base_class would return B as the answer since A is an abstract_class. def base_class - class_of_active_record_descendant(self) + unless self < ActiveRecord::Tag + raise ActiveRecordError, "#{name} doesn't belong in a hierarchy descending from ActiveRecord" + end + + sup = active_record_super + if sup == Base || sup == Model || sup.abstract_class? + self + else + sup.base_class + end end # Set this to true if this is an abstract class (see <tt>abstract_class?</tt>). + # If you are using inheritance with ActiveRecord and don't want child classes + # to utilize the implied STI table name of the parent class, this will need to be true. + # For example, given the following: + # + # class SuperClass < ActiveRecord::Base + # self.abstract_class = true + # end + # class Child < SuperClass + # self.table_name = 'the_table_i_really_want' + # end + # + # + # <tt>self.abstract_class = true</tt> is required to make <tt>Child<.find,.create, or any Arel method></tt> use <tt>the_table_i_really_want</tt> instead of a table called <tt>super_classes</tt> + # attr_accessor :abstract_class # Returns whether this class is an abstract class or not. @@ -63,26 +92,9 @@ module ActiveRecord # single-table inheritance model that makes it possible to create # objects of different types from the same table. def instantiate(record, column_types = {}) - 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 - column_types = sti_class.decorate_columns(column_types) - instance = sti_class.allocate.init_with('attributes' => record, - 'column_types' => column_types) - end - - instance + sti_class = find_sti_class(record[inheritance_column]) + column_types = sti_class.decorate_columns(column_types) + sti_class.allocate.init_with('attributes' => record, 'column_types' => column_types) end # For internal use. @@ -95,21 +107,6 @@ module ActiveRecord protected - # Returns the class descending directly from ActiveRecord::Base or an - # abstract class, if any, in the inheritance hierarchy. - def class_of_active_record_descendant(klass) - unless klass < Model - raise ActiveRecordError, "#{name} doesn't belong in a hierarchy descending from ActiveRecord" - end - - sup = klass.active_record_super - if [Base, Model].include?(klass) || [Base, Model].include?(sup) || sup.abstract_class? - klass - else - class_of_active_record_descendant(sup) - end - end - # Returns the class type of the record using the current module as a prefix. So descendants of # MyApp::Business::Account would appear as MyApp::Business::AccountSubclass. def compute_type(type_name) diff --git a/activerecord/lib/active_record/integration.rb b/activerecord/lib/active_record/integration.rb index 2c42f4cca5..23c272ef12 100644 --- a/activerecord/lib/active_record/integration.rb +++ b/activerecord/lib/active_record/integration.rb @@ -39,7 +39,7 @@ module ActiveRecord when new_record? "#{self.class.model_name.cache_key}/new" when timestamp = self[:updated_at] - timestamp = timestamp.utc.to_s(:number) + timestamp = timestamp.utc.to_s(:nsec) "#{self.class.model_name.cache_key}/#{id}-#{timestamp}" else "#{self.class.model_name.cache_key}/#{id}" diff --git a/activerecord/lib/active_record/locking/optimistic.rb b/activerecord/lib/active_record/locking/optimistic.rb index 4d73cdd37a..e96ed00f9c 100644 --- a/activerecord/lib/active_record/locking/optimistic.rb +++ b/activerecord/lib/active_record/locking/optimistic.rb @@ -1,4 +1,9 @@ module ActiveRecord + ActiveSupport.on_load(:active_record_config) do + mattr_accessor :lock_optimistically, instance_accessor: false + self.lock_optimistically = true + end + module Locking # == What is Optimistic Locking # @@ -51,8 +56,7 @@ module ActiveRecord extend ActiveSupport::Concern included do - config_attribute :lock_optimistically, :global => true - self.lock_optimistically = true + config_attribute :lock_optimistically end def locking_enabled? #:nodoc: @@ -82,9 +86,9 @@ module ActiveRecord stmt = relation.where( relation.table[self.class.primary_key].eq(id).and( - relation.table[lock_col].eq(quote_value(previous_lock_value)) + relation.table[lock_col].eq(self.class.quote_value(previous_lock_value)) ) - ).arel.compile_update(arel_attributes_values(false, false, attribute_names)) + ).arel.compile_update(arel_attributes_with_values_for_update(attribute_names)) affected_rows = connection.update stmt @@ -101,24 +105,29 @@ module ActiveRecord end end - def destroy #:nodoc: - return super unless locking_enabled? + def destroy_row + affected_rows = super - if persisted? - table = self.class.arel_table - lock_col = self.class.locking_column - predicate = table[self.class.primary_key].eq(id). - and(table[lock_col].eq(send(lock_col).to_i)) + if locking_enabled? && affected_rows != 1 + raise ActiveRecord::StaleObjectError.new(self, "destroy") + end - affected_rows = self.class.unscoped.where(predicate).delete_all + affected_rows + end - unless affected_rows == 1 - raise ActiveRecord::StaleObjectError.new(self, "destroy") - end + def relation_for_destroy + relation = super + + if locking_enabled? + column_name = self.class.locking_column + column = self.class.columns_hash[column_name] + substitute = connection.substitute_at(column, relation.bind_values.length) + + relation = relation.where(self.class.arel_table[column_name].eq(substitute)) + relation.bind_values << [column, self[column_name].to_i] end - @destroyed = true - freeze + relation end module ClassMethods @@ -133,8 +142,7 @@ module ActiveRecord # Set the column to use for optimistic locking. Defaults to +lock_version+. def locking_column=(value) - @original_locking_column = @locking_column if defined?(@locking_column) - @locking_column = value.to_s + @locking_column = value.to_s end # The version column used for optimistic locking. Defaults to +lock_version+. @@ -160,16 +168,16 @@ module ActiveRecord super end - # If the locking column has no default value set, - # start the lock version at zero. Note we can't use - # <tt>locking_enabled?</tt> at this point as - # <tt>@attributes</tt> may not have been initialized yet. - def initialize_attributes(attributes) #:nodoc: - if attributes.key?(locking_column) && lock_optimistically - attributes[locking_column] ||= 0 - end + def column_defaults + @column_defaults ||= begin + defaults = super - attributes + if defaults.key?(locking_column) && lock_optimistically + defaults[locking_column] ||= 0 + end + + defaults + end end end end diff --git a/activerecord/lib/active_record/migration.rb b/activerecord/lib/active_record/migration.rb index 2a9139749d..c1d57855a9 100644 --- a/activerecord/lib/active_record/migration.rb +++ b/activerecord/lib/active_record/migration.rb @@ -1,7 +1,4 @@ -require "active_support/core_ext/module/delegation" require "active_support/core_ext/class/attribute_accessors" -require 'active_support/deprecation' -require 'active_record/schema_migration' require 'set' module ActiveRecord @@ -33,6 +30,12 @@ module ActiveRecord end end + class PendingMigrationError < ActiveRecordError#:nodoc: + def initialize + super("Migrations are pending run 'rake db:migrate RAILS_ENV=#{ENV['RAILS_ENV']}' to resolve the issue") + end + end + # = Active Record Migrations # # Migrations can manage the evolution of a schema used by several physical @@ -47,7 +50,7 @@ module ActiveRecord # # class AddSsl < ActiveRecord::Migration # def up - # add_column :accounts, :ssl_enabled, :boolean, :default => 1 + # add_column :accounts, :ssl_enabled, :boolean, :default => true # end # # def down @@ -327,10 +330,28 @@ module ActiveRecord class Migration autoload :CommandRecorder, 'active_record/migration/command_recorder' + + # This class is used to verify that all migrations have been run before + # loading a web page if config.active_record.migration_error is set to :page_load + class CheckPending + def initialize(app) + @app = app + end + + def call(env) + ActiveRecord::Migration.check_pending! + @app.call(env) + end + end + class << self attr_accessor :delegate # :nodoc: end + def self.check_pending! + raise ActiveRecord::PendingMigrationError if ActiveRecord::Migrator.needs_migration? + end + def self.method_missing(name, *args, &block) # :nodoc: (delegate || superclass.delegate).send(name, *args, &block) end @@ -606,6 +627,14 @@ module ActiveRecord end end + def needs_migration? + current_version < last_version + end + + def last_version + migrations(migrations_paths).last.try(:version)||0 + end + def proper_table_name(name) # Use the Active Record objects own table_name, or pre/suffix from ActiveRecord::Base if name is a symbol/string name.table_name rescue "#{ActiveRecord::Base.table_name_prefix}#{name}#{ActiveRecord::Base.table_name_suffix}" diff --git a/activerecord/lib/active_record/migration/command_recorder.rb b/activerecord/lib/active_record/migration/command_recorder.rb index 96b62fdd61..95f4360578 100644 --- a/activerecord/lib/active_record/migration/command_recorder.rb +++ b/activerecord/lib/active_record/migration/command_recorder.rb @@ -51,13 +51,15 @@ module ActiveRecord super || delegate.respond_to?(*args) end - [:create_table, :create_join_table, :change_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| + [:create_table, :create_join_table, :change_table, :rename_table, :add_column, :remove_column, :rename_index, :rename_column, :add_index, :remove_index, :add_timestamps, :remove_timestamps, :change_column, :change_column_default, :add_reference, :remove_reference].each do |method| class_eval <<-EOV, __FILE__, __LINE__ + 1 def #{method}(*args) # def create_table(*args) record(:"#{method}", args) # record(:create_table, args) end # end EOV end + alias :add_belongs_to :add_reference + alias :remove_belongs_to :remove_reference private @@ -102,6 +104,16 @@ module ActiveRecord [:remove_timestamps, args] end + def invert_add_reference(args) + [:remove_reference, args] + end + alias :invert_add_belongs_to :invert_add_reference + + def invert_remove_reference(args) + [:add_reference, args] + end + alias :invert_remove_belongs_to :invert_remove_reference + # Forwards any missing method call to the \target. def method_missing(method, *args, &block) @delegate.send(method, *args, &block) diff --git a/activerecord/lib/active_record/migration/join_table.rb b/activerecord/lib/active_record/migration/join_table.rb index 01a580781b..e880ae97bb 100644 --- a/activerecord/lib/active_record/migration/join_table.rb +++ b/activerecord/lib/active_record/migration/join_table.rb @@ -4,13 +4,11 @@ module ActiveRecord private def find_join_table_name(table_1, table_2, options = {}) - options.delete(:table_name) { join_table_name(table_1, table_2) } + options.delete(:table_name) || join_table_name(table_1, table_2) end def join_table_name(table_1, table_2) - tables_names = [table_1, table_2].map(&:to_s).sort - - tables_names.join("_").to_sym + [table_1, table_2].sort.join("_").to_sym end end end diff --git a/activerecord/lib/active_record/model.rb b/activerecord/lib/active_record/model.rb index 86de5ab2fa..16d9d404e3 100644 --- a/activerecord/lib/active_record/model.rb +++ b/activerecord/lib/active_record/model.rb @@ -1,49 +1,68 @@ -require 'active_support/deprecation' +require 'active_support/core_ext/module/attribute_accessors' module ActiveRecord - # <tt>ActiveRecord::Model</tt> can be included into a class to add Active Record persistence. - # This is an alternative to inheriting from <tt>ActiveRecord::Base</tt>. Example: + module Configuration # :nodoc: + # This just abstracts out how we define configuration options in AR. Essentially we + # have mattr_accessors on the ActiveRecord:Model constant that define global defaults. + # Classes that then use AR get class_attributes defined, which means that when they + # are assigned the default will be overridden for that class and subclasses. (Except + # when options[:global] == true, in which case there is one global value always.) + def config_attribute(name, options = {}) + if options[:global] + class_eval <<-CODE, __FILE__, __LINE__ + 1 + def self.#{name}; ActiveRecord::Model.#{name}; end + def #{name}; ActiveRecord::Model.#{name}; end + def self.#{name}=(val); ActiveRecord::Model.#{name} = val; end + CODE + else + options[:instance_writer] ||= false + class_attribute name, options + + singleton_class.class_eval <<-CODE, __FILE__, __LINE__ + 1 + remove_method :#{name} + def #{name}; ActiveRecord::Model.#{name}; end + CODE + end + end + end + + # This allows us to detect an ActiveRecord::Model while it's in the process of + # being included. + module Tag; end + + # <tt>ActiveRecord::Model</tt> can be included into a class to add Active Record + # persistence. This is an alternative to inheriting from <tt>ActiveRecord::Base</tt>. # # class Post # include ActiveRecord::Model # end - # module Model - module ClassMethods #:nodoc: - include ActiveSupport::Callbacks::ClassMethods - include ActiveModel::Naming - include QueryCache::ClassMethods - include ActiveSupport::Benchmarkable - include ActiveSupport::DescendantsTracker - - include Querying - include Translation - include DynamicMatchers - include CounterCache - include Explain - include ConnectionHandling - end - - def self.included(base) - return if base.singleton_class < ClassMethods + extend ActiveSupport::Concern + extend ConnectionHandling + extend ActiveModel::Observing::ClassMethods + def self.append_features(base) base.class_eval do - extend ClassMethods - Callbacks::Register.setup(self) - initialize_generated_modules unless self == Base + include Tag + extend Configuration end + + super end - extend ActiveModel::Configuration - extend ActiveModel::Callbacks - extend ActiveModel::MassAssignmentSecurity::ClassMethods - extend ActiveModel::AttributeMethods::ClassMethods - extend Callbacks::Register - extend Explain - extend ConnectionHandling + included do + extend ActiveModel::Naming + extend ActiveSupport::Benchmarkable + extend ActiveSupport::DescendantsTracker + + extend QueryCache::ClassMethods + extend Querying + extend Translation + extend DynamicMatchers + extend Explain + extend ConnectionHandling - def self.extend(*modules) - ClassMethods.send(:include, *modules) + initialize_generated_modules unless self == Base end include Persistence @@ -52,18 +71,26 @@ module ActiveRecord include Inheritance include Scoping include Sanitization - include Integration include AttributeAssignment include ActiveModel::Conversion + include Integration include Validations - include Locking::Optimistic, Locking::Pessimistic + include CounterCache + include Locking::Optimistic + include Locking::Pessimistic include AttributeMethods - include Callbacks, ActiveModel::Observing, Timestamp + include Callbacks + include ActiveModel::Observing + include Timestamp include Associations - include IdentityMap include ActiveModel::SecurePassword - include AutosaveAssociation, NestedAttributes - include Aggregations, Transactions, Reflection, Serialization, Store + include AutosaveAssociation + include NestedAttributes + include Aggregations + include Transactions + include Reflection + include Serialization + include Store include Core class << self @@ -75,35 +102,69 @@ module ActiveRecord false end + # Defines the name of the table column which will store the class name on single-table + # inheritance situations. + # + # The default inheritance column name is +type+, which means it's a + # reserved word inside Active Record. To be able to use single-table + # inheritance with another column name, or to use the column +type+ in + # your own model for something else, you can override this method to + # return a different name: + # + # def self.inheritance_column + # 'zoink' + # end def inheritance_column 'type' end end - module DeprecationProxy #:nodoc: - class << self - instance_methods.each { |m| undef_method m unless m =~ /^__|^object_id$|^instance_eval$/ } - - def method_missing(name, *args, &block) - if Model.respond_to?(name) - Model.send(name, *args, &block) - else - ActiveSupport::Deprecation.warn( - "The object passed to the active_record load hook was previously ActiveRecord::Base " \ - "(a Class). Now it is ActiveRecord::Model (a Module). You have called `#{name}' which " \ - "is only defined on ActiveRecord::Base. Please change your code so that it works with " \ - "a module rather than a class. (Model is included in Base, so anything added to Model " \ - "will be available on Base as well.)" - ) - Base.send(name, *args, &block) - end + class DeprecationProxy < BasicObject #:nodoc: + def initialize(model = Model, base = Base) + @model = model + @base = base + end + + def method_missing(name, *args, &block) + if @model.respond_to?(name, true) + @model.send(name, *args, &block) + else + ::ActiveSupport::Deprecation.warn( + "The object passed to the active_record load hook was previously ActiveRecord::Base " \ + "(a Class). Now it is ActiveRecord::Model (a Module). You have called `#{name}' which " \ + "is only defined on ActiveRecord::Base. Please change your code so that it works with " \ + "a module rather than a class. (Model is included in Base, so anything added to Model " \ + "will be available on Base as well.)" + ) + @base.send(name, *args, &block) end + end + + alias send method_missing - alias send method_missing + def extend(*mods) + ::ActiveSupport::Deprecation.warn( + "The object passed to the active_record load hook was previously ActiveRecord::Base " \ + "(a Class). Now it is ActiveRecord::Model (a Module). You have called `extend' which " \ + "would add singleton methods to Model. This is presumably not what you want, since the " \ + "methods would not be inherited down to Base. Rather than using extend, please use " \ + "ActiveSupport::Concern + include, which will ensure that your class methods are " \ + "inherited." + ) + @base.extend(*mods) end end end + # This hook is where config accessors on Model get defined. + # + # We don't want to just open the Model module and add stuff to it in other files, because + # that would cause Model to load, which causes all sorts of loading order issues. + # + # We need this hook rather than just using the :active_record one, because users of the + # :active_record hook may need to use config options. + ActiveSupport.run_load_hooks(:active_record_config, Model) + # Load Base at this point, because the active_record load hook is run in that file. Base end diff --git a/activerecord/lib/active_record/model_schema.rb b/activerecord/lib/active_record/model_schema.rb index b8764217d3..99de16cd33 100644 --- a/activerecord/lib/active_record/model_schema.rb +++ b/activerecord/lib/active_record/model_schema.rb @@ -1,7 +1,18 @@ -require 'active_support/concern' -require 'active_support/core_ext/class/attribute_accessors' module ActiveRecord + ActiveSupport.on_load(:active_record_config) do + mattr_accessor :primary_key_prefix_type, instance_accessor: false + + mattr_accessor :table_name_prefix, instance_accessor: false + self.table_name_prefix = "" + + mattr_accessor :table_name_suffix, instance_accessor: false + self.table_name_suffix = "" + + mattr_accessor :pluralize_table_names, instance_accessor: false + self.pluralize_table_names = true + end + module ModelSchema extend ActiveSupport::Concern @@ -13,7 +24,7 @@ module ActiveRecord # 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. - config_attribute :primary_key_prefix_type, :global => true + config_attribute :primary_key_prefix_type, global: true ## # :singleton-method: @@ -26,14 +37,12 @@ module ActiveRecord # a namespace by defining a singleton method in the parent module called table_name_prefix which # returns your chosen prefix. config_attribute :table_name_prefix - self.table_name_prefix = "" ## # :singleton-method: # Works like +table_name_prefix+, but appends instead of prepends (set to "_basecamp" gives "projects_basecamp", # "people_basecamp"). By default, the suffix is the empty string. config_attribute :table_name_suffix - self.table_name_suffix = "" ## # :singleton-method: @@ -41,7 +50,6 @@ module ActiveRecord # If true, the default table name for a Product class will be +products+. If false, it would just be +product+. # See table_name for the full rules on table/class naming. This is true, by default. config_attribute :pluralize_table_names - self.pluralize_table_names = true end module ClassMethods @@ -114,11 +122,18 @@ module ActiveRecord # You can also just define your own <tt>self.table_name</tt> method; see # the documentation for ActiveRecord::Base#table_name. def table_name=(value) - @original_table_name = @table_name if defined?(@table_name) - @table_name = value && value.to_s - @quoted_table_name = nil - @arel_table = nil - @relation = Relation.new(self, arel_table) + value = value && value.to_s + + if defined?(@table_name) + return if value == @table_name + reset_column_information if connected? + end + + @table_name = value + @quoted_table_name = nil + @arel_table = nil + @sequence_name = nil unless defined?(@explicit_sequence_name) && @explicit_sequence_name + @relation = Relation.new(self, arel_table) end # Returns a quoted version of the table name, used to construct SQL statements. @@ -128,16 +143,12 @@ module ActiveRecord # Computes the table name, (re)sets it internally, and returns it. def reset_table_name #:nodoc: - if abstract_class? - self.table_name = if active_record_super == Base || active_record_super.abstract_class? - nil - else - active_record_super.table_name - end + self.table_name = if abstract_class? + active_record_super == Base ? nil : active_record_super.table_name elsif active_record_super.abstract_class? - self.table_name = active_record_super.table_name || compute_table_name + active_record_super.table_name || compute_table_name else - self.table_name = compute_table_name + compute_table_name end end @@ -152,8 +163,8 @@ module ActiveRecord # Sets the value of inheritance_column def inheritance_column=(value) - @original_inheritance_column = inheritance_column - @inheritance_column = value.to_s + @inheritance_column = value.to_s + @explicit_inheritance_column = true end def sequence_name @@ -165,7 +176,8 @@ module ActiveRecord end def reset_sequence_name #:nodoc: - self.sequence_name = connection.default_sequence_name(table_name, primary_key) + @explicit_sequence_name = false + @sequence_name = connection.default_sequence_name(table_name, primary_key) end # Sets the name of the sequence to use when generating ids to the given @@ -183,8 +195,8 @@ module ActiveRecord # self.sequence_name = "projectseq" # default would have been "project_seq" # end def sequence_name=(value) - @original_sequence_name = @sequence_name if defined?(@sequence_name) @sequence_name = value.to_s + @explicit_sequence_name = true end # Indicates whether the table associated with this class exists @@ -213,7 +225,7 @@ module ActiveRecord def decorate_columns(columns_hash) # :nodoc: return if columns_hash.empty? - serialized_attributes.keys.each do |key| + serialized_attributes.each_key do |key| columns_hash[key] = AttributeMethods::Serialization::Type.new(columns_hash[key]) end @@ -247,13 +259,12 @@ module ActiveRecord # and true as the value. This makes it possible to do O(1) lookups in respond_to? to check if a given method for attribute # is available. def column_methods_hash #:nodoc: - @dynamic_methods_hash ||= column_names.inject(Hash.new(false)) do |methods, attr| + @dynamic_methods_hash ||= column_names.each_with_object(Hash.new(false)) do |attr, methods| attr_name = attr.to_s methods[attr.to_sym] = attr_name methods["#{attr}=".to_sym] = attr_name methods["#{attr}?".to_sym] = attr_name methods["#{attr}_before_type_cast".to_sym] = attr_name - methods end end @@ -296,12 +307,15 @@ module ActiveRecord @column_types = nil @content_columns = nil @dynamic_methods_hash = nil - @inheritance_column = nil + @inheritance_column = nil unless defined?(@explicit_inheritance_column) && @explicit_inheritance_column @relation = nil end - def clear_cache! # :nodoc: - connection.schema_cache.clear! + # This is a hook for use by modules that need to do extra stuff to + # attributes when they are initialized. (e.g. attribute + # serialization) + def initialize_attributes(attributes, options = {}) #:nodoc: + attributes end private @@ -309,8 +323,7 @@ module ActiveRecord # Guesses the table name, but does not decorate it with prefix and suffix information. def undecorated_table_name(class_name = base_class.name) table_name = class_name.to_s.demodulize.underscore - table_name = table_name.pluralize if pluralize_table_names - table_name + pluralize_table_names ? table_name.pluralize : table_name end # Computes and returns a table name according to default conventions. diff --git a/activerecord/lib/active_record/nested_attributes.rb b/activerecord/lib/active_record/nested_attributes.rb index 9e21039c4f..2e7fb3bbb3 100644 --- a/activerecord/lib/active_record/nested_attributes.rb +++ b/activerecord/lib/active_record/nested_attributes.rb @@ -1,10 +1,13 @@ 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 + ActiveSupport.on_load(:active_record_config) do + mattr_accessor :nested_attributes_options, instance_accessor: false + self.nested_attributes_options = {} + end + module NestedAttributes #:nodoc: class TooManyRecords < ActiveRecordError end @@ -13,16 +16,15 @@ module ActiveRecord included do config_attribute :nested_attributes_options - self.nested_attributes_options = {} end # = Active Record Nested Attributes # # Nested attributes allow you to save attributes on associated records - # through the parent. By default nested attribute updating is turned off, - # you can enable it using the accepts_nested_attributes_for class method. - # When you enable nested attributes an attribute writer is defined on - # the model. + # through the parent. By default nested attribute updating is turned off + # and you can enable it using the accepts_nested_attributes_for class + # method. When you enable nested attributes an attribute writer is + # defined on the model. # # The attribute writer is named after the association, which means that # in the following example, two new methods are added to your model: @@ -192,18 +194,6 @@ module ActiveRecord # 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 @@ -222,9 +212,7 @@ module ActiveRecord module ClassMethods REJECT_ALL_BLANK_PROC = proc { |attributes| attributes.all? { |key, value| key == '_destroy' || value.blank? } } - # Defines an attributes writer for the specified association(s). If you - # are using <tt>attr_protected</tt> or <tt>attr_accessible</tt>, then you - # will need to add the attribute writer to the allowed list. + # Defines an attributes writer for the specified association(s). # # Supported options: # [:allow_destroy] @@ -243,15 +231,24 @@ module ActiveRecord # any value for _destroy. # [:limit] # Allows you to specify the maximum number of the associated records that - # can be processed with the nested attributes. If the size of the + # can be processed with the nested attributes. Limit also can be specified as a + # Proc or a Symbol pointing to a method that should return number. If the size of the # nested attributes array exceeds the specified limit, NestedAttributes::TooManyRecords # exception is raised. If omitted, any number associations can be processed. # Note that the :limit option is only applicable to one-to-many associations. # [:update_only] - # Allows you to specify that an existing record may only be updated. - # A new record may only be created when there is no existing record. - # This option only works for one-to-one associations and is ignored for - # collection associations. This option is off by default. + # For a one-to-one association, this option allows you to specify how + # nested attributes are to be used when an associated record already + # exists. In general, an existing record may either be updated with the + # new set of attribute values or be replaced by a wholly new record + # containing those values. By default the :update_only option is +false+ + # and the nested attributes are used to update the existing record only + # if they include the record's <tt>:id</tt> value. Otherwise a new + # record will be instantiated and used to replace the existing one. + # However if the :update_only option is +true+, the nested attributes + # are used to update the record's attributes always, regardless of + # whether the <tt>:id</tt> is present. The option is ignored for collection + # associations. # # Examples: # # creates avatar_attributes= @@ -280,12 +277,12 @@ module ActiveRecord # def pirate_attributes=(attributes) # assign_nested_attributes_for_one_to_one_association(:pirate, attributes, mass_assignment_options) # end - class_eval <<-eoruby, __FILE__, __LINE__ + 1 + generated_feature_methods.module_eval <<-eoruby, __FILE__, __LINE__ + 1 if method_defined?(:#{association_name}_attributes=) remove_method(:#{association_name}_attributes=) end def #{association_name}_attributes=(attributes) - assign_nested_attributes_for_#{type}_association(:#{association_name}, attributes, mass_assignment_options) + assign_nested_attributes_for_#{type}_association(:#{association_name}, attributes) end eoruby else @@ -312,31 +309,34 @@ module ActiveRecord # Assigns the given attributes to the association. # - # If update_only is false and the given attributes include an <tt>:id</tt> - # that matches the existing record's id, then the existing record will be - # modified. If update_only is true, a new record is only created when no - # object exists. Otherwise a new record will be built. + # If an associated record does not yet exist, one will be instantiated. If + # an associated record already exists, the method's behavior depends on + # the value of the update_only option. If update_only is +false+ and the + # given attributes include an <tt>:id</tt> that matches the existing record's + # id, then the existing record will be modified. If no <tt>:id</tt> is provided + # it will be replaced with a new record. If update_only is +true+ the existing + # record will be modified regardless of whether an <tt>:id</tt> is provided. # # If the given attributes include a matching <tt>:id</tt> attribute, or # 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, assignment_opts = {}) + def assign_nested_attributes_for_one_to_one_association(association_name, attributes) options = self.nested_attributes_options[association_name] attributes = attributes.with_indifferent_access 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], assignment_opts) unless call_reject_if(association_name, attributes) + assign_to_or_mark_for_destruction(record, attributes, options[:allow_destroy]) unless call_reject_if(association_name, attributes) - elsif attributes['id'].present? && !assignment_opts[:without_protection] + 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}" if respond_to?(method) - send(method, attributes.except(*unassignable_keys(assignment_opts)), assignment_opts) + send(method, attributes.except(*UNASSIGNABLE_KEYS)) else - raise ArgumentError, "Cannot build association #{association_name}. Are you trying to build a polymorphic one-to-one association?" + raise ArgumentError, "Cannot build association `#{association_name}'. Are you trying to build a polymorphic one-to-one association?" end end end @@ -358,7 +358,7 @@ module ActiveRecord # }) # # Will update the name of the Person with ID 1, build a new associated - # person with the name `John', and mark the associated Person with ID 2 + # person with the name 'John', and mark the associated Person with ID 2 # for destruction. # # Also accepts an Array of attribute hashes: @@ -368,15 +368,26 @@ module ActiveRecord # { :name => 'John' }, # { :id => '2', :_destroy => true } # ]) - def assign_nested_attributes_for_collection_association(association_name, attributes_collection, assignment_opts = {}) + def assign_nested_attributes_for_collection_association(association_name, attributes_collection) 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})" end - if options[:limit] && attributes_collection.size > options[:limit] - raise TooManyRecords, "Maximum #{options[:limit]} records are allowed. Got #{attributes_collection.size} records instead." + if limit = options[:limit] + limit = case limit + when Symbol + send(limit) + when Proc + limit.call + else + limit + end + + if limit && attributes_collection.size > limit + raise TooManyRecords, "Maximum #{limit} records are allowed. Got #{attributes_collection.size} records instead." + end end if attributes_collection.is_a? Hash @@ -394,7 +405,7 @@ module ActiveRecord association.target else attribute_ids = attributes_collection.map {|a| a['id'] || a[:id] }.compact - attribute_ids.empty? ? [] : association.scoped.where(association.klass.primary_key => attribute_ids) + attribute_ids.empty? ? [] : association.scope.where(association.klass.primary_key => attribute_ids) end attributes_collection.each do |attributes| @@ -402,7 +413,7 @@ module ActiveRecord if attributes['id'].blank? unless reject_new_record?(association_name, attributes) - association.build(attributes.except(*unassignable_keys(assignment_opts)), assignment_opts) + association.build(attributes.except(*UNASSIGNABLE_KEYS)) end elsif existing_record = existing_records.detect { |record| record.id.to_s == attributes['id'].to_s } unless association.loaded? || call_reject_if(association_name, attributes) @@ -415,14 +426,11 @@ module ActiveRecord 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], assignment_opts) + assign_to_or_mark_for_destruction(existing_record, attributes, options[:allow_destroy]) end - elsif assignment_opts[:without_protection] - association.build(attributes.except(*unassignable_keys(assignment_opts)), assignment_opts) else raise_nested_attributes_record_not_found(association_name, attributes['id']) end @@ -431,8 +439,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, assignment_opts) - record.assign_attributes(attributes.except(*unassignable_keys(assignment_opts)), assignment_opts) + def assign_to_or_mark_for_destruction(record, attributes, allow_destroy) + record.assign_attributes(attributes.except(*UNASSIGNABLE_KEYS)) record.mark_for_destruction if has_destroy_flag?(attributes) && allow_destroy end @@ -461,9 +469,5 @@ module ActiveRecord 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 - - def unassignable_keys(assignment_opts) - assignment_opts[:without_protection] ? UNASSIGNABLE_KEYS - %w[id] : UNASSIGNABLE_KEYS - end end end diff --git a/activerecord/lib/active_record/null_relation.rb b/activerecord/lib/active_record/null_relation.rb index 60c37ac2b7..4c1c91e3df 100644 --- a/activerecord/lib/active_record/null_relation.rb +++ b/activerecord/lib/active_record/null_relation.rb @@ -1,10 +1,61 @@ # -*- coding: utf-8 -*- module ActiveRecord - # = Active Record Null Relation - class NullRelation < Relation + module NullRelation # :nodoc: def exec_queries @records = [] end + + def pluck(_column_name) + [] + end + + def delete_all(_conditions = nil) + 0 + end + + def update_all(_updates, _conditions = nil, _options = {}) + 0 + end + + def delete(_id_or_array) + 0 + end + + def size + 0 + end + + def empty? + true + end + + def any? + false + end + + def many? + false + end + + def to_sql + @to_sql ||= "" + end + + def where_values_hash + {} + end + + def count + 0 + end + + def calculate(_operation, _column_name, _options = {}) + nil + end + + def exists?(_id = false) + false + end end -end
\ No newline at end of file +end diff --git a/activerecord/lib/active_record/observer.rb b/activerecord/lib/active_record/observer.rb index fdf17c003c..6b2f6f98a5 100644 --- a/activerecord/lib/active_record/observer.rb +++ b/activerecord/lib/active_record/observer.rb @@ -1,4 +1,3 @@ -require 'active_support/core_ext/class/attribute' module ActiveRecord # = Active Record Observer @@ -74,6 +73,12 @@ module ActiveRecord # # Observers will not be invoked unless you define these in your application configuration. # + # If you are using Active Record outside Rails, activate the observers explicitly in a configuration or + # environment file: + # + # ActiveRecord::Base.add_observer CommentObserver.instance + # ActiveRecord::Base.add_observer SignupObserver.instance + # # == Loading # # Observers register themselves in the model class they observe, since it is the class that diff --git a/activerecord/lib/active_record/persistence.rb b/activerecord/lib/active_record/persistence.rb index 9bc046c775..2eaad1d469 100644 --- a/activerecord/lib/active_record/persistence.rb +++ b/activerecord/lib/active_record/persistence.rb @@ -1,5 +1,3 @@ -require 'active_support/concern' - module ActiveRecord # = Active Record Persistence module Persistence @@ -19,12 +17,6 @@ module ActiveRecord # # Create a single new object # User.create(:first_name => 'Jamie') # - # # Create a single new object using the :admin mass-assignment security role - # User.create({ :first_name => 'Jamie', :is_admin => true }, :as => :admin) - # - # # Create a single new object bypassing mass-assignment security - # User.create({ :first_name => 'Jamie', :is_admin => true }, :without_protection => true) - # # # Create an Array of new objects # User.create([{ :first_name => 'Jamie' }, { :first_name => 'Jeremy' }]) # @@ -37,11 +29,11 @@ module ActiveRecord # User.create([{ :first_name => 'Jamie' }, { :first_name => 'Jeremy' }]) do |u| # u.is_admin = false # end - def create(attributes = nil, options = {}, &block) + def create(attributes = nil, &block) if attributes.is_a?(Array) - attributes.collect { |attr| create(attr, options, &block) } + attributes.collect { |attr| create(attr, &block) } else - object = new(attributes, options, &block) + object = new(attributes, &block) object.save object end @@ -115,36 +107,37 @@ module ActiveRecord # callbacks, Observer methods, or any <tt>:dependent</tt> association # options, use <tt>#destroy</tt>. def delete - if persisted? - self.class.delete(id) - IdentityMap.remove(self) if IdentityMap.enabled? - end + self.class.delete(id) if persisted? @destroyed = true freeze end # Deletes the record in the database and freezes this instance to reflect # that no changes should be made (since they can't be persisted). + # + # There's a series of callbacks associated with <tt>destroy</tt>. If + # the <tt>before_destroy</tt> callback return +false+ the action is cancelled + # and <tt>destroy</tt> returns +false+. See + # ActiveRecord::Callbacks for further details. def destroy + raise ReadOnlyRecord if readonly? destroy_associations - - if persisted? - IdentityMap.remove(self) if IdentityMap.enabled? - pk = self.class.primary_key - column = self.class.columns_hash[pk] - substitute = connection.substitute_at(column, 0) - - relation = self.class.unscoped.where( - self.class.arel_table[pk].eq(substitute)) - - relation.bind_values = [[column, id]] - relation.delete_all - end - + destroy_row if persisted? @destroyed = true freeze end + # Deletes the record in the database and freezes this instance to reflect + # that no changes should be made (since they can't be persisted). + # + # There's a series of callbacks associated with <tt>destroy!</tt>. If + # the <tt>before_destroy</tt> callback return +false+ the action is cancelled + # and <tt>destroy!</tt> raises ActiveRecord::RecordNotDestroyed. See + # ActiveRecord::Callbacks for further details. + def destroy! + destroy || raise(ActiveRecord::RecordNotDestroyed) + 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 @@ -162,7 +155,7 @@ module ActiveRecord became.instance_variable_set("@new_record", new_record?) became.instance_variable_set("@destroyed", destroyed?) became.instance_variable_set("@errors", errors) - became.type = klass.name unless self.class.descends_from_active_record? + became.public_send("#{klass.inheritance_column}=", klass.name) unless self.class.descends_from_active_record? became end @@ -176,55 +169,68 @@ module ActiveRecord # def update_attribute(name, value) name = name.to_s - raise ActiveRecordError, "#{name} is marked as readonly" if self.class.readonly_attributes.include?(name) + verify_readonly_attribute(name) send("#{name}=", value) save(:validate => false) end - # Updates a single attribute of an object, without calling save. - # - # * Validation is skipped. - # * Callbacks are skipped. - # * updated_at/updated_on column is not updated if that column is available. - # - # Raises an +ActiveRecordError+ when called on new objects, or when the +name+ - # attribute is marked as readonly. - def update_column(name, value) - name = name.to_s - raise ActiveRecordError, "#{name} is marked as readonly" if self.class.readonly_attributes.include?(name) - raise ActiveRecordError, "can not update on a new record object" unless persisted? - raw_write_attribute(name, value) - self.class.update_all({ name => value }, self.class.primary_key => id) == 1 - end - # Updates the attributes of the model from the passed-in hash and saves the # record, all wrapped in a transaction. If the object is invalid, the saving # will fail and false will be returned. - # - # When updating model attributes, mass-assignment security protection is respected. - # If no +:as+ option is supplied then the +:default+ role will be used. - # If you want to bypass the protection given by +attr_protected+ and - # +attr_accessible+ then you can do so using the +:without_protection+ option. - def update_attributes(attributes, options = {}) + def update_attributes(attributes) # The following transaction covers any possible database side-effects of the # attributes assignment. For example, setting the IDs of a child collection. with_transaction_returning_status do - assign_attributes(attributes, options) + assign_attributes(attributes) save end end # Updates its receiver just like +update_attributes+ but calls <tt>save!</tt> instead # of +save+, so an exception is raised if the record is invalid. - def update_attributes!(attributes, options = {}) + def update_attributes!(attributes) # The following transaction covers any possible database side-effects of the # attributes assignment. For example, setting the IDs of a child collection. with_transaction_returning_status do - assign_attributes(attributes, options) + assign_attributes(attributes) save! end end + # Updates a single attribute of an object, without calling save. + # + # * Validation is skipped. + # * Callbacks are skipped. + # * updated_at/updated_on column is not updated if that column is available. + # + # Raises an +ActiveRecordError+ when called on new objects, or when the +name+ + # attribute is marked as readonly. + def update_column(name, value) + update_columns(name => value) + end + + # Updates the attributes from the passed-in hash, without calling save. + # + # * Validation is skipped. + # * Callbacks are skipped. + # * updated_at/updated_on column is not updated if that column is available. + # + # Raises an +ActiveRecordError+ when called on new objects, or when at least + # one of the attributes is marked as readonly. + def update_columns(attributes) + raise ActiveRecordError, "can not update on a new record object" unless persisted? + + attributes.each_key do |key| + verify_readonly_attribute(key.to_s) + end + + attributes.each do |k,v| + raw_write_attribute(k,v) + end + + self.class.where(self.class.primary_key => id).update_all(attributes) == 1 + end + # Initializes +attribute+ to zero if +nil+ and adds the value passed as +by+ (default is 1). # The increment is performed directly on the underlying attribute, no setter is invoked. # Only makes sense for number-based attributes. Returns +self+. @@ -284,11 +290,15 @@ module ActiveRecord clear_aggregation_cache clear_association_cache - IdentityMap.without do - fresh_object = self.class.unscoped { self.class.find(id, options) } - @attributes.update(fresh_object.instance_variable_get('@attributes')) - @columns_hash = fresh_object.instance_variable_get('@columns_hash') - end + fresh_object = + if options && options[:lock] + self.class.unscoped { self.class.lock.find(id) } + else + self.class.unscoped { self.class.find(id) } + end + + @attributes.update(fresh_object.instance_variable_get('@attributes')) + @columns_hash = fresh_object.instance_variable_get('@columns_hash') @attributes_cache = {} self @@ -323,14 +333,15 @@ module ActiveRecord changes = {} attributes.each do |column| - changes[column.to_s] = write_attribute(column.to_s, current_time) + column = column.to_s + changes[column] = write_attribute(column, 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.unscoped.update_all(changes, { primary_key => self[primary_key] }) == 1 + self.class.unscoped.where(primary_key => self[primary_key]).update_all(changes) == 1 end end @@ -340,6 +351,22 @@ module ActiveRecord def destroy_associations end + def destroy_row + relation_for_destroy.delete_all + end + + def relation_for_destroy + pk = self.class.primary_key + column = self.class.columns_hash[pk] + substitute = connection.substitute_at(column, 0) + + relation = self.class.unscoped.where( + self.class.arel_table[pk].eq(substitute)) + + relation.bind_values = [[column, id]] + relation + end + def create_or_update raise ReadOnlyRecord if readonly? result = new_record? ? create : update @@ -349,7 +376,7 @@ module ActiveRecord # Updates the associated record with values matching those of the instance attributes. # Returns the number of affected rows. def update(attribute_names = @attributes.keys) - attributes_with_values = arel_attributes_values(false, false, attribute_names) + attributes_with_values = arel_attributes_with_values_for_update(attribute_names) return 0 if attributes_with_values.empty? klass = self.class stmt = klass.unscoped.where(klass.arel_table[klass.primary_key].eq(id)).arel.compile_update(attributes_with_values) @@ -359,15 +386,17 @@ module ActiveRecord # Creates a record with values matching those of the instance attributes # and returns its id. def create - attributes_values = arel_attributes_values(!id.nil?) + attributes_values = arel_attributes_with_values_for_create(!id.nil?) new_id = self.class.unscoped.insert attributes_values - self.id ||= new_id if self.class.primary_key - IdentityMap.add(self) if IdentityMap.enabled? @new_record = false id end + + def verify_readonly_attribute(name) + raise ActiveRecordError, "#{name} is marked as readonly" if self.class.readonly_attributes.include?(name) + end end end diff --git a/activerecord/lib/active_record/query_cache.rb b/activerecord/lib/active_record/query_cache.rb index 9701898415..2bd8ecda20 100644 --- a/activerecord/lib/active_record/query_cache.rb +++ b/activerecord/lib/active_record/query_cache.rb @@ -1,4 +1,3 @@ -require 'active_support/core_ext/object/blank' module ActiveRecord # = Active Record Query Cache @@ -34,16 +33,22 @@ module ActiveRecord response = @app.call(env) response[2] = Rack::BodyProxy.new(response[2]) do - ActiveRecord::Base.connection_id = connection_id - ActiveRecord::Base.connection.clear_query_cache - ActiveRecord::Base.connection.disable_query_cache! unless enabled + restore_query_cache_settings(connection_id, enabled) end response rescue Exception => e + restore_query_cache_settings(connection_id, enabled) + raise e + end + + private + + def restore_query_cache_settings(connection_id, enabled) + ActiveRecord::Base.connection_id = connection_id ActiveRecord::Base.connection.clear_query_cache ActiveRecord::Base.connection.disable_query_cache! unless enabled - raise e end + end end diff --git a/activerecord/lib/active_record/querying.rb b/activerecord/lib/active_record/querying.rb index 0e6fecbc4b..13e09eda53 100644 --- a/activerecord/lib/active_record/querying.rb +++ b/activerecord/lib/active_record/querying.rb @@ -1,16 +1,15 @@ -require 'active_support/core_ext/module/delegation' -require 'active_support/deprecation' module ActiveRecord module Querying - delegate :find, :first, :first!, :last, :last!, :all, :exists?, :any?, :many?, :to => :scoped - delegate :first_or_create, :first_or_create!, :first_or_initialize, :to => :scoped - delegate :destroy, :destroy_all, :delete, :delete_all, :update, :update_all, :to => :scoped - delegate :find_each, :find_in_batches, :to => :scoped + delegate :find, :take, :take!, :first, :first!, :last, :last!, :exists?, :any?, :many?, :to => :all + delegate :first_or_create, :first_or_create!, :first_or_initialize, :to => :all + delegate :find_by, :find_by!, :to => :all + delegate :destroy, :destroy_all, :delete, :delete_all, :update, :update_all, :to => :all + delegate :find_each, :find_in_batches, :to => :all delegate :select, :group, :order, :except, :reorder, :limit, :offset, :joins, :where, :preload, :eager_load, :includes, :from, :lock, :readonly, - :having, :create_with, :uniq, :references, :none, :to => :scoped - delegate :count, :average, :minimum, :maximum, :sum, :calculate, :pluck, :to => :scoped + :having, :create_with, :uniq, :references, :none, :to => :all + delegate :count, :average, :minimum, :maximum, :sum, :calculate, :pluck, :ids, :to => :all # Executes a custom SQL query against your database and returns all the results. The results will # be returned as an array with columns requested encapsulated as attributes of the model you call @@ -61,8 +60,10 @@ module ActiveRecord # # Product.count_by_sql "SELECT COUNT(*) FROM sales s, customers c WHERE s.customer_id = c.id" def count_by_sql(sql) - sql = sanitize_conditions(sql) - connection.select_value(sql, "#{name} Count").to_i + logging_query_plan do + sql = sanitize_conditions(sql) + connection.select_value(sql, "#{name} Count").to_i + end end end end diff --git a/activerecord/lib/active_record/railtie.rb b/activerecord/lib/active_record/railtie.rb index 058dd58efb..a9f80ccd5f 100644 --- a/activerecord/lib/active_record/railtie.rb +++ b/activerecord/lib/active_record/railtie.rb @@ -29,7 +29,13 @@ module ActiveRecord 'ActiveRecord::RecordNotSaved' => :unprocessable_entity ) + + config.active_record.use_schema_cache_dump = true + + config.eager_load_namespaces << ActiveRecord + rake_tasks do + require "active_record/base" load "active_record/railties/databases.rake" end @@ -38,10 +44,15 @@ module ActiveRecord # first time. Also, make it output to STDERR. console do |app| require "active_record/railties/console_sandbox" if app.sandbox? + require "active_record/base" console = ActiveSupport::Logger.new(STDERR) Rails.logger.extend ActiveSupport::Logger.broadcast console end + runner do |app| + require "active_record/base" + end + initializer "active_record.initialize_timezone" do ActiveSupport.on_load(:active_record) do self.time_zone_aware_attributes = true @@ -53,16 +64,34 @@ 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) + initializer "active_record.migration_error" do |app| + if config.active_record.delete(:migration_error) == :page_load + config.app_middleware.insert_after "::ActionDispatch::Callbacks", + "ActiveRecord::Migration::CheckPending" + end + end + + initializer "active_record.check_schema_cache_dump" do + if config.active_record.delete(:use_schema_cache_dump) + config.after_initialize do |app| + ActiveSupport.on_load(:active_record) do + filename = File.join(app.config.paths["db"].first, "schema_cache.dump") + + if File.file?(filename) + cache = Marshal.load File.binread filename + if cache.version == ActiveRecord::Migrator.current_version + ActiveRecord::Model.connection.schema_cache = cache + else + warn "schema_cache.dump is expired. Current version is #{ActiveRecord::Migrator.current_version}, but cache version is #{cache.version}." + end + end + end + end + end end initializer "active_record.set_configs" do |app| ActiveSupport.on_load(:active_record) do - if app.config.active_record.delete(:whitelist_attributes) - attr_accessible(nil) - end app.config.active_record.each do |k,v| send "#{k}=", v end @@ -73,7 +102,9 @@ module ActiveRecord # and then establishes the connection. initializer "active_record.initialize_database" do |app| ActiveSupport.on_load(:active_record) do - self.configurations = app.config.database_configuration + unless ENV['DATABASE_URL'] + self.configurations = app.config.database_configuration + end establish_connection end end @@ -87,18 +118,12 @@ module ActiveRecord end initializer "active_record.set_reloader_hooks" do |app| - hook = lambda do - ActiveRecord::Base.clear_reloadable_connections! - ActiveRecord::Base.clear_cache! - end + hook = app.config.reload_classes_only_on_change ? :to_prepare : :to_cleanup - if app.config.reload_classes_only_on_change - ActiveSupport.on_load(:active_record) do - ActionDispatch::Reloader.to_prepare(&hook) - end - else - ActiveSupport.on_load(:active_record) do - ActionDispatch::Reloader.to_cleanup(&hook) + ActiveSupport.on_load(:active_record) do + ActionDispatch::Reloader.send(hook) do + ActiveRecord::Model.clear_reloadable_connections! + ActiveRecord::Model.clear_cache! end end end @@ -107,14 +132,15 @@ module ActiveRecord config.watchable_files.concat ["#{app.root}/db/schema.rb", "#{app.root}/db/structure.sql"] end - config.after_initialize do + config.after_initialize do |app| ActiveSupport.on_load(:active_record) do - ActiveRecord::Base.instantiate_observers + ActiveRecord::Model.instantiate_observers ActionDispatch::Reloader.to_prepare do - ActiveRecord::Base.instantiate_observers + ActiveRecord::Model.instantiate_observers end end + end end end diff --git a/activerecord/lib/active_record/railties/console_sandbox.rb b/activerecord/lib/active_record/railties/console_sandbox.rb index 65a3d68619..90b462fad6 100644 --- a/activerecord/lib/active_record/railties/console_sandbox.rb +++ b/activerecord/lib/active_record/railties/console_sandbox.rb @@ -1,6 +1,4 @@ -ActiveRecord::Base.connection.increment_open_transactions ActiveRecord::Base.connection.begin_db_transaction at_exit do ActiveRecord::Base.connection.rollback_db_transaction - ActiveRecord::Base.connection.decrement_open_transactions end diff --git a/activerecord/lib/active_record/railties/databases.rake b/activerecord/lib/active_record/railties/databases.rake index 14bb0a724e..d134978128 100644 --- a/activerecord/lib/active_record/railties/databases.rake +++ b/activerecord/lib/active_record/railties/databases.rake @@ -1,8 +1,7 @@ -require 'active_support/core_ext/object/inclusion' require 'active_record' db_namespace = namespace :db do - task :load_config => :rails_env do + task :load_config do ActiveRecord::Base.configurations = Rails.application.config.database_configuration ActiveRecord::Migrator.migrations_paths = Rails.application.paths['db/migrate'].to_a @@ -14,139 +13,36 @@ db_namespace = namespace :db do end namespace :create do - # desc 'Create all the local databases defined in config/database.yml' task :all => :load_config do - ActiveRecord::Base.configurations.each_value do |config| - # Skip entries that don't have a database key, such as the first entry here: - # - # defaults: &defaults - # adapter: mysql - # username: root - # password: - # host: localhost - # - # development: - # database: blog_development - # *defaults - next unless config['database'] - # Only connect to local databases - local_database?(config) { create_database(config) } - end + ActiveRecord::Tasks::DatabaseTasks.create_all end end - desc 'Create the database from config/database.yml for the current Rails.env (use db:create:all to create all dbs in the config)' - task :create => :load_config do - configs_for_environment.each { |config| create_database(config) } - ActiveRecord::Base.establish_connection(configs_for_environment.first) - end - - def mysql_creation_options(config) - @charset = ENV['CHARSET'] || 'utf8' - @collation = ENV['COLLATION'] || 'utf8_unicode_ci' - {:charset => (config['charset'] || @charset), :collation => (config['collation'] || @collation)} - end - - def create_database(config) - begin - if config['adapter'] =~ /sqlite/ - if File.exist?(config['database']) - $stderr.puts "#{config['database']} already exists" - else - begin - # Create the SQLite database - ActiveRecord::Base.establish_connection(config) - ActiveRecord::Base.connection - rescue Exception => e - $stderr.puts e, *(e.backtrace) - $stderr.puts "Couldn't create database for #{config.inspect}" - end - end - return # Skip the else clause of begin/rescue - else - ActiveRecord::Base.establish_connection(config) - ActiveRecord::Base.connection - end - rescue - case config['adapter'] - when /mysql/ - if config['adapter'] =~ /jdbc/ - #FIXME After Jdbcmysql gives this class - require 'active_record/railties/jdbcmysql_error' - error_class = ArJdbcMySQL::Error - else - error_class = config['adapter'] =~ /mysql2/ ? Mysql2::Error : Mysql::Error - end - access_denied_error = 1045 - begin - ActiveRecord::Base.establish_connection(config.merge('database' => nil)) - ActiveRecord::Base.connection.create_database(config['database'], mysql_creation_options(config)) - ActiveRecord::Base.establish_connection(config) - 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']}.* " \ - "TO '#{config['username']}'@'localhost' " \ - "IDENTIFIED BY '#{config['password']}' WITH GRANT OPTION;" - ActiveRecord::Base.establish_connection(config.merge( - 'database' => nil, 'username' => 'root', 'password' => root_password)) - ActiveRecord::Base.connection.create_database(config['database'], mysql_creation_options(config)) - ActiveRecord::Base.connection.execute grant_statement - ActiveRecord::Base.establish_connection(config) - else - $stderr.puts sqlerr.error - $stderr.puts "Couldn't create database for #{config.inspect}, charset: #{config['charset'] || @charset}, collation: #{config['collation'] || @collation}" - $stderr.puts "(if you set the charset manually, make sure you have a matching collation)" if config['charset'] - end - end - when /postgresql/ - @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 Exception => e - $stderr.puts e, *(e.backtrace) - $stderr.puts "Couldn't create database for #{config.inspect}" - end - end + desc 'Create the database from DATABASE_URL or config/database.yml for the current Rails.env (use db:create:all to create all dbs in the config)' + task :create => [:load_config] do + if ENV['DATABASE_URL'] + ActiveRecord::Tasks::DatabaseTasks.create_database_url else - $stderr.puts "#{config['database']} already exists" + ActiveRecord::Tasks::DatabaseTasks.create_current end end namespace :drop do - # desc 'Drops all the local databases defined in config/database.yml' task :all => :load_config do - ActiveRecord::Base.configurations.each_value do |config| - # Skip entries that don't have a database key - next unless config['database'] - begin - # Only connect to local databases - local_database?(config) { drop_database(config) } - rescue Exception => e - $stderr.puts "Couldn't drop #{config['database']} : #{e.inspect}" - end - end + ActiveRecord::Tasks::DatabaseTasks.drop_all end end - desc 'Drops the database for the current Rails.env (use db:drop:all to drop all databases)' - task :drop => :load_config do - configs_for_environment.each { |config| drop_database_and_rescue(config) } - end - - def local_database?(config, &block) - if config['host'].in?(['127.0.0.1', 'localhost']) || config['host'].blank? - yield + desc 'Drops the database using DATABASE_URL or the current Rails.env (use db:drop:all to drop all databases)' + task :drop => [:load_config] do + if ENV['DATABASE_URL'] + ActiveRecord::Tasks::DatabaseTasks.drop_database_url else - $stderr.puts "This task only modifies local databases. #{config['database']} is on a remote host." + ActiveRecord::Tasks::DatabaseTasks.drop_current end end - - desc "Migrate the database (options: VERSION=x, VERBOSE=false)." + desc "Migrate the database (options: VERSION=x, VERBOSE=false, SCOPE=blog)." task :migrate => [:environment, :load_config] do ActiveRecord::Migration.verbose = ENV["VERBOSE"] ? ENV["VERBOSE"] == "true" : true ActiveRecord::Migrator.migrate(ActiveRecord::Migrator.migrations_paths, ENV["VERSION"] ? ENV["VERSION"].to_i : nil) do |migration| @@ -200,8 +96,6 @@ db_namespace = namespace :db do desc 'Display status of migrations' 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) puts 'Schema migrations table does not exist yet.' next # means "return" for rake task @@ -222,7 +116,7 @@ db_namespace = namespace :db do ['up', version, '********** NO FILE **********'] end # output - puts "\ndatabase: #{config['database']}\n\n" + puts "\ndatabase: #{ActiveRecord::Base.connection_config[:database]}\n\n" puts "#{'Status'.center(8)} #{'Migration ID'.ljust(14)} Migration Name" puts "-" * 50 (db_list + file_list).sort_by {|migration| migration[1]}.each do |migration| @@ -247,48 +141,32 @@ db_namespace = namespace :db do end # desc 'Drops and recreates the database from db/schema.rb for the current environment and loads the seeds.' - task :reset => :environment do + task :reset => [:environment, :load_config] do db_namespace["drop"].invoke db_namespace["setup"].invoke end # desc "Retrieves the charset for the current environment's database" - task :charset => :environment do - config = ActiveRecord::Base.configurations[Rails.env || 'development'] - case config['adapter'] - when /mysql/ - ActiveRecord::Base.establish_connection(config) - puts ActiveRecord::Base.connection.charset - when /postgresql/ - ActiveRecord::Base.establish_connection(config) - puts ActiveRecord::Base.connection.encoding - when /sqlite/ - ActiveRecord::Base.establish_connection(config) - puts ActiveRecord::Base.connection.encoding - else - $stderr.puts 'sorry, your database adapter is not supported yet, feel free to submit a patch' - end + task :charset => [:environment, :load_config] do + puts ActiveRecord::Tasks::DatabaseTasks.charset_current end # desc "Retrieves the collation for the current environment's database" - task :collation => :environment do - config = ActiveRecord::Base.configurations[Rails.env || 'development'] - case config['adapter'] - when /mysql/ - ActiveRecord::Base.establish_connection(config) - puts ActiveRecord::Base.connection.collation - else - $stderr.puts 'sorry, your database adapter is not supported yet, feel free to submit a patch' + task :collation => [:environment, :load_config] do + begin + puts ActiveRecord::Tasks::DatabaseTasks.collation_current + rescue NoMethodError + $stderr.puts 'Sorry, your database adapter is not supported yet, feel free to submit a patch' end end desc 'Retrieves the current schema version number' - task :version => :environment do + task :version => [:environment, :load_config] do puts "Current version: #{ActiveRecord::Migrator.current_version}" end # desc "Raises an error if there are pending migrations" - task :abort_if_pending_migrations => :environment do + task :abort_if_pending_migrations => [:environment, :load_config] do pending_migrations = ActiveRecord::Migrator.new(:up, ActiveRecord::Migrator.migrations_paths).pending_migrations if pending_migrations.any? @@ -311,20 +189,19 @@ db_namespace = namespace :db do namespace :fixtures do desc "Load fixtures into the current environment's database. Load specific fixtures using FIXTURES=x,y. Load from subdirectory in test/fixtures using FIXTURES_DIR=z. Specify an alternative path (eg. spec/fixtures) using FIXTURES_PATH=spec/fixtures." - task :load => :environment do + task :load => [:environment, :load_config] do require 'active_record/fixtures' - ActiveRecord::Base.establish_connection(Rails.env) base_dir = File.join [Rails.root, ENV['FIXTURES_PATH'] || %w{test fixtures}].flatten fixtures_dir = File.join [base_dir, ENV['FIXTURES_DIR']].compact - (ENV['FIXTURES'] ? ENV['FIXTURES'].split(/,/) : Dir["#{fixtures_dir}/**/*.{yml,csv}"].map {|f| f[(fixtures_dir.size + 1)..-5] }).each do |fixture_file| + (ENV['FIXTURES'] ? ENV['FIXTURES'].split(/,/) : Dir["#{fixtures_dir}/**/*.yml"].map {|f| f[(fixtures_dir.size + 1)..-5] }).each do |fixture_file| ActiveRecord::Fixtures.create_fixtures(fixtures_dir, fixture_file) end end # desc "Search for a fixture given a LABEL or ID. Specify an alternative path (eg. spec/fixtures) using FIXTURES_PATH=spec/fixtures." - task :identify => :environment do + task :identify => [:environment, :load_config] do require 'active_record/fixtures' label, id = ENV['LABEL'], ENV['ID'] @@ -353,14 +230,13 @@ db_namespace = namespace :db do require 'active_record/schema_dumper' filename = ENV['SCHEMA'] || "#{Rails.root}/db/schema.rb" File.open(filename, "w:utf-8") do |file| - ActiveRecord::Base.establish_connection(Rails.env) ActiveRecord::SchemaDumper.dump(ActiveRecord::Base.connection, file) end db_namespace['schema:dump'].reenable end desc 'Load a schema.rb file into the database' - task :load => :environment do + task :load => [:environment, :load_config] do file = ENV['SCHEMA'] || "#{Rails.root}/db/schema.rb" if File.exists?(file) load(file) @@ -369,82 +245,90 @@ db_namespace = namespace :db do end end - task :load_if_ruby => 'db:create' do + task :load_if_ruby => ['db:create', :environment] do db_namespace["schema:load"].invoke if ActiveRecord::Base.schema_format == :ruby end + + namespace :cache do + desc 'Create a db/schema_cache.dump file.' + task :dump => [:environment, :load_config] do + con = ActiveRecord::Base.connection + filename = File.join(Rails.application.config.paths["db"].first, "schema_cache.dump") + + con.schema_cache.clear! + con.tables.each { |table| con.schema_cache.add(table) } + open(filename, 'wb') { |f| f.write(Marshal.dump(con.schema_cache)) } + end + + desc 'Clear a db/schema_cache.dump file.' + task :clear => [:environment, :load_config] do + filename = File.join(Rails.application.config.paths["db"].first, "schema_cache.dump") + FileUtils.rm(filename) if File.exists?(filename) + end + end + end namespace :structure do + def set_firebird_env(config) + ENV['ISC_USER'] = config['username'].to_s if config['username'] + ENV['ISC_PASSWORD'] = config['password'].to_s if config['password'] + end + + def firebird_db_string(config) + FireRuby::Database.db_string_for(config.symbolize_keys) + end + desc 'Dump the database structure to db/structure.sql. Specify another file with DB_STRUCTURE=db/my_structure.sql' - task :dump => :environment do - abcs = ActiveRecord::Base.configurations + task :dump => [:environment, :load_config] do filename = ENV['DB_STRUCTURE'] || File.join(Rails.root, "db", "structure.sql") - case abcs[Rails.env]['adapter'] - when /mysql/, 'oci', 'oracle' - ActiveRecord::Base.establish_connection(abcs[Rails.env]) + current_config = ActiveRecord::Tasks::DatabaseTasks.current_config + case current_config['adapter'] + when /mysql/, /postgresql/, /sqlite/ + ActiveRecord::Tasks::DatabaseTasks.structure_dump(current_config, filename) + when 'oci', 'oracle' + ActiveRecord::Base.establish_connection(current_config) File.open(filename, "w:utf-8") { |f| f << ActiveRecord::Base.connection.structure_dump } - when /postgresql/ - set_psql_env(abcs[Rails.env]) - search_path = abcs[Rails.env]['schema_search_path'] - unless search_path.blank? - search_path = search_path.split(",").map{|search_path_part| "--schema=#{search_path_part.strip}" }.join(" ") - end - `pg_dump -i -s -x -O -f #{filename} #{search_path} #{abcs[Rails.env]['database']}` - raise 'Error dumping database' if $?.exitstatus == 1 - when /sqlite/ - dbfile = abcs[Rails.env]['database'] - `sqlite3 #{dbfile} .schema > #{filename}` when 'sqlserver' - `smoscript -s #{abcs[Rails.env]['host']} -d #{abcs[Rails.env]['database']} -u #{abcs[Rails.env]['username']} -p #{abcs[Rails.env]['password']} -f #{filename} -A -U` + `smoscript -s #{current_config['host']} -d #{current_config['database']} -u #{current_config['username']} -p #{current_config['password']} -f #{filename} -A -U` when "firebird" - set_firebird_env(abcs[Rails.env]) - db_string = firebird_db_string(abcs[Rails.env]) + set_firebird_env(current_config) + db_string = firebird_db_string(current_config) sh "isql -a #{db_string} > #{filename}" else - raise "Task not supported by '#{abcs[Rails.env]["adapter"]}'" + raise "Task not supported by '#{current_config["adapter"]}'" end if ActiveRecord::Base.connection.supports_migrations? File.open(filename, "a") { |f| f << ActiveRecord::Base.connection.dump_schema_information } end + db_namespace['structure:dump'].reenable end # desc "Recreate the databases from the structure.sql file" task :load => [:environment, :load_config] do - env = ENV['RAILS_ENV'] || 'test' - - abcs = ActiveRecord::Base.configurations + current_config = ActiveRecord::Tasks::DatabaseTasks.current_config(:env => (ENV['RAILS_ENV'] || 'test')) filename = ENV['DB_STRUCTURE'] || File.join(Rails.root, "db", "structure.sql") - case abcs[env]['adapter'] - when /mysql/ - ActiveRecord::Base.establish_connection(abcs[env]) - ActiveRecord::Base.connection.execute('SET foreign_key_checks = 0') - IO.read(filename).split("\n\n").each do |table| - ActiveRecord::Base.connection.execute(table) - end - when /postgresql/ - set_psql_env(abcs[env]) - `psql -f "#{filename}" #{abcs[env]['database']}` - when /sqlite/ - dbfile = abcs[env]['database'] - `sqlite3 #{dbfile} < "#{filename}"` + case current_config['adapter'] + when /mysql/, /postgresql/, /sqlite/ + ActiveRecord::Tasks::DatabaseTasks.structure_load(current_config, filename) when 'sqlserver' - `sqlcmd -S #{abcs[env]['host']} -d #{abcs[env]['database']} -U #{abcs[env]['username']} -P #{abcs[env]['password']} -i #{filename}` + `sqlcmd -S #{current_config['host']} -d #{current_config['database']} -U #{current_config['username']} -P #{current_config['password']} -i #{filename}` when 'oci', 'oracle' - ActiveRecord::Base.establish_connection(abcs[env]) + ActiveRecord::Base.establish_connection(current_config) IO.read(filename).split(";\n\n").each do |ddl| ActiveRecord::Base.connection.execute(ddl) end when 'firebird' - set_firebird_env(abcs[env]) - db_string = firebird_db_string(abcs[env]) + set_firebird_env(current_config) + db_string = firebird_db_string(current_config) sh "isql -i #{filename} #{db_string}" else - raise "Task not supported by '#{abcs[env]['adapter']}'" + raise "Task not supported by '#{current_config['adapter']}'" end end - task :load_if_sql => 'db:create' do + task :load_if_sql => ['db:create', :environment] do db_namespace["structure:load"].invoke if ActiveRecord::Base.schema_format == :sql end end @@ -458,46 +342,48 @@ db_namespace = namespace :db do db_namespace["test:load_schema"].invoke when :sql db_namespace["test:load_structure"].invoke - end + end + end + + # desc "Recreate the test database from an existent schema.rb file" + task :load_schema => 'db:test:purge' do + ActiveRecord::Base.establish_connection(ActiveRecord::Base.configurations['test']) + ActiveRecord::Schema.verbose = false + db_namespace["schema:load"].invoke end # desc "Recreate the test database from an existent structure.sql file" task :load_structure => 'db:test:purge' do begin - old_env, ENV['RAILS_ENV'] = ENV['RAILS_ENV'], 'test' + ActiveRecord::Tasks::DatabaseTasks.current_config(:config => ActiveRecord::Base.configurations['test']) db_namespace["structure:load"].invoke ensure - ENV['RAILS_ENV'] = old_env + ActiveRecord::Tasks::DatabaseTasks.current_config(:config => nil) end end - # desc "Recreate the test database from an existent schema.rb file" - task :load_schema => 'db:test:purge' do - ActiveRecord::Base.establish_connection(ActiveRecord::Base.configurations['test']) - ActiveRecord::Schema.verbose = false - db_namespace["schema:load"].invoke + # desc "Recreate the test database from a fresh schema" + task :clone do + case ActiveRecord::Base.schema_format + when :ruby + db_namespace["test:clone_schema"].invoke + when :sql + db_namespace["test:clone_structure"].invoke + end end # desc "Recreate the test database from a fresh schema.rb file" - task :clone => %w(db:schema:dump db:test:load_schema) + task :clone_schema => ["db:schema:dump", "db:test:load_schema"] # desc "Recreate the test database from a fresh structure.sql file" task :clone_structure => [ "db:structure:dump", "db:test:load_structure" ] # desc "Empty the test database" - task :purge => :environment do + task :purge => [:environment, :load_config] do abcs = ActiveRecord::Base.configurations case abcs['test']['adapter'] - when /mysql/ - ActiveRecord::Base.establish_connection(:test) - ActiveRecord::Base.connection.recreate_database(abcs['test']['database'], mysql_creation_options(abcs['test'])) - when /postgresql/ - ActiveRecord::Base.clear_active_connections! - drop_database(abcs['test']) - create_database(abcs['test']) - when /sqlite/ - dbfile = abcs['test']['database'] - File.delete(dbfile) if File.exist?(dbfile) + when /mysql/, /postgresql/, /sqlite/ + ActiveRecord::Tasks::DatabaseTasks.purge abcs['test'] when 'sqlserver' test = abcs.deep_dup['test'] test_database = test['database'] @@ -520,25 +406,10 @@ db_namespace = namespace :db do # desc 'Check for pending migrations and load the test schema' task :prepare => 'db:abort_if_pending_migrations' do unless ActiveRecord::Base.configurations.blank? - db_namespace[{ :sql => 'test:clone_structure', :ruby => 'test:load' }[ActiveRecord::Base.schema_format]].invoke + db_namespace['test:load'].invoke end end end - - namespace :sessions do - # desc "Creates a sessions migration for use with ActiveRecord::SessionStore" - task :create => :environment do - raise 'Task unavailable to this database (no migration support)' unless ActiveRecord::Base.connection.supports_migrations? - Rails.application.load_generators - require 'rails/generators/rails/session_migration/session_migration_generator' - Rails::Generators::SessionMigrationGenerator.start [ ENV['MIGRATION'] || 'add_sessions_table' ] - end - - # desc "Clear the sessions table" - task :clear => :environment do - ActiveRecord::Base.connection.execute "DELETE FROM #{session_table_name}" - end - end end namespace :railties do @@ -546,8 +417,8 @@ namespace :railties do # desc "Copies missing migrations from Railties (e.g. 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 = ActiveSupport::OrderedHash.new - Rails.application.railties.all do |railtie| + railties = {} + Rails.application.railties.each do |railtie| next unless to_load == :all || to_load.include?(railtie.railtie_name) if railtie.respond_to?(:paths) && (path = railtie.paths['db/migrate'].first) @@ -563,7 +434,7 @@ namespace :railties do puts "Copied migration #{migration.basename} from #{name}" end - ActiveRecord::Migration.copy( ActiveRecord::Migrator.migrations_paths.first, railties, + ActiveRecord::Migration.copy(ActiveRecord::Migrator.migrations_paths.first, railties, :on_skip => on_skip, :on_copy => on_copy) end end @@ -571,53 +442,3 @@ end task 'test:prepare' => 'db:test:prepare' -def drop_database(config) - case config['adapter'] - when /mysql/ - ActiveRecord::Base.establish_connection(config) - ActiveRecord::Base.connection.drop_database config['database'] - when /sqlite/ - require 'pathname' - path = Pathname.new(config['database']) - file = path.absolute? ? path.to_s : File.join(Rails.root, path) - - FileUtils.rm(file) - when /postgresql/ - ActiveRecord::Base.establish_connection(config.merge('database' => 'postgres', 'schema_search_path' => 'public')) - ActiveRecord::Base.connection.drop_database config['database'] - end -end - -def drop_database_and_rescue(config) - begin - drop_database(config) - rescue Exception => e - $stderr.puts "Couldn't drop #{config['database']} : #{e.inspect}" - end -end - -def configs_for_environment - environments = [Rails.env] - environments << 'test' if Rails.env.development? - ActiveRecord::Base.configurations.values_at(*environments).compact.reject { |config| config['database'].blank? } -end - -def session_table_name - ActiveRecord::SessionStore::Session.table_name -end - -def set_firebird_env(config) - ENV['ISC_USER'] = config['username'].to_s if config['username'] - ENV['ISC_PASSWORD'] = config['password'].to_s if config['password'] -end - -def firebird_db_string(config) - FireRuby::Database.db_string_for(config.symbolize_keys) -end - -def set_psql_env(config) - ENV['PGHOST'] = config['host'] if config['host'] - ENV['PGPORT'] = config['port'].to_s if config['port'] - ENV['PGPASSWORD'] = config['password'].to_s if config['password'] - ENV['PGUSER'] = config['username'].to_s if config['username'] -end diff --git a/activerecord/lib/active_record/railties/jdbcmysql_error.rb b/activerecord/lib/active_record/railties/jdbcmysql_error.rb index 0b75983484..6a38211bff 100644 --- a/activerecord/lib/active_record/railties/jdbcmysql_error.rb +++ b/activerecord/lib/active_record/railties/jdbcmysql_error.rb @@ -1,6 +1,6 @@ #FIXME Remove if ArJdbcMysql will give. module ArJdbcMySQL #:nodoc: - class Error < StandardError + class Error < StandardError #:nodoc: attr_accessor :error_number, :sql_state def initialize msg diff --git a/activerecord/lib/active_record/readonly_attributes.rb b/activerecord/lib/active_record/readonly_attributes.rb index 836b15e2ce..b3c20c4aff 100644 --- a/activerecord/lib/active_record/readonly_attributes.rb +++ b/activerecord/lib/active_record/readonly_attributes.rb @@ -1,12 +1,10 @@ -require 'active_support/concern' -require 'active_support/core_ext/class/attribute' module ActiveRecord module ReadonlyAttributes extend ActiveSupport::Concern included do - config_attribute :_attr_readonly + class_attribute :_attr_readonly, instance_accessor: false self._attr_readonly = [] end @@ -22,5 +20,10 @@ module ActiveRecord self._attr_readonly end end + + def _attr_readonly + ActiveSupport::Deprecation.warn("Instance level _attr_readonly method is deprecated, please use class level method.") + defined?(@_attr_readonly) ? @_attr_readonly : self.class._attr_readonly + end end end diff --git a/activerecord/lib/active_record/reflection.rb b/activerecord/lib/active_record/reflection.rb index 8f8a3ec3bb..f322b96f79 100644 --- a/activerecord/lib/active_record/reflection.rb +++ b/activerecord/lib/active_record/reflection.rb @@ -1,5 +1,3 @@ -require 'active_support/core_ext/class/attribute' -require 'active_support/core_ext/object/inclusion' module ActiveRecord # = Active Record Reflection @@ -7,8 +5,7 @@ module ActiveRecord extend ActiveSupport::Concern included do - extend ActiveModel::Configuration - config_attribute :reflections + class_attribute :reflections self.reflections = {} end @@ -21,13 +18,13 @@ module ActiveRecord # MacroReflection class has info for AggregateReflection and AssociationReflection # classes. module ClassMethods - def create_reflection(macro, name, options, active_record) + def create_reflection(macro, name, scope, options, active_record) case macro - when :has_many, :belongs_to, :has_one, :has_and_belongs_to_many - klass = options[:through] ? ThroughReflection : AssociationReflection - reflection = klass.new(macro, name, options, active_record) - when :composed_of - reflection = AggregateReflection.new(macro, name, options, active_record) + when :has_many, :belongs_to, :has_one, :has_and_belongs_to_many + klass = options[:through] ? ThroughReflection : AssociationReflection + reflection = klass.new(macro, name, scope, options, active_record) + when :composed_of + reflection = AggregateReflection.new(macro, name, scope, options, active_record) end self.reflections = self.reflections.merge(name => reflection) @@ -44,7 +41,8 @@ module ActiveRecord # Account.reflect_on_aggregation(:balance) # => the balance AggregateReflection # def reflect_on_aggregation(aggregation) - reflections[aggregation].is_a?(AggregateReflection) ? reflections[aggregation] : nil + reflection = reflections[aggregation] + reflection if reflection.is_a?(AggregateReflection) end # Returns an array of AssociationReflection objects for all the @@ -68,7 +66,8 @@ module ActiveRecord # Invoice.reflect_on_association(:line_items).macro # returns :has_many # def reflect_on_association(association) - reflections[association].is_a?(AssociationReflection) ? reflections[association] : nil + reflection = reflections[association] + reflection if reflection.is_a?(AssociationReflection) end # Returns an array of AssociationReflection objects for all associations which have <tt>:autosave</tt> enabled. @@ -77,7 +76,6 @@ module ActiveRecord end end - # Abstract base class for AggregateReflection and AssociationReflection. Objects of # AggregateReflection and AssociationReflection are returned by the Reflection::ClassMethods. class MacroReflection @@ -93,6 +91,8 @@ module ActiveRecord # <tt>has_many :clients</tt> returns <tt>:has_many</tt> attr_reader :macro + attr_reader :scope + # Returns the hash of options used for the macro. # # <tt>composed_of :balance, :class_name => 'Money'</tt> returns <tt>{ :class_name => "Money" }</tt> @@ -103,9 +103,10 @@ module ActiveRecord attr_reader :plural_name # :nodoc: - def initialize(macro, name, options, active_record) + def initialize(macro, name, scope, options, active_record) @macro = macro @name = name + @scope = scope @options = options @active_record = active_record @plural_name = active_record.pluralize_table_names ? @@ -138,10 +139,6 @@ module ActiveRecord active_record == other_aggregation.active_record end - def sanitized_conditions #:nodoc: - @sanitized_conditions ||= klass.send(:sanitize_sql, options[:conditions]) if options[:conditions] - end - private def derive_class_name name.to_s.camelize @@ -152,6 +149,10 @@ module ActiveRecord # Holds all the meta-data about an aggregation as it was specified in the # Active Record class. class AggregateReflection < MacroReflection #:nodoc: + def mapping + mapping = options[:mapping] || [name, name] + mapping.first.is_a?(Array) ? mapping : [mapping] + end end # Holds all the meta-data about an association as it was specified in the @@ -173,15 +174,15 @@ module ActiveRecord @klass ||= active_record.send(:compute_type, class_name) end - def initialize(macro, name, options, active_record) + def initialize(*args) super @collection = [:has_many, :has_and_belongs_to_many].include?(macro) end # Returns a new, unsaved instance of the associated class. +options+ will # be passed to the class's constructor. - def build_association(*options, &block) - klass.new(*options, &block) + def build_association(attributes, &block) + klass.new(attributes, &block) end def table_name @@ -192,6 +193,10 @@ module ActiveRecord @quoted_table_name ||= klass.quoted_table_name end + def join_table + @join_table ||= options[:join_table] || derive_join_table + end + def foreign_key @foreign_key ||= options[:foreign_key] || derive_foreign_key end @@ -239,6 +244,10 @@ module ActiveRecord def check_validity! check_validity_of_inverse! + + if has_and_belongs_to_many? && association_foreign_key == foreign_key + raise HasAndBelongsToManyAssociationForeignKeyNeeded.new(self) + end end def check_validity_of_inverse! @@ -267,11 +276,10 @@ module ActiveRecord false 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 - [[options[:conditions]].compact] + # An array of arrays of scopes. Each item in the outside array corresponds to a reflection + # in the #chain. + def scope_chain + scope ? [[scope]] : [[]] end alias :source_macro :macro @@ -321,6 +329,10 @@ module ActiveRecord macro == :belongs_to end + def has_and_belongs_to_many? + macro == :has_and_belongs_to_many + end + def association_class case macro when :belongs_to @@ -346,6 +358,10 @@ module ActiveRecord end end + def polymorphic? + options.key? :polymorphic + end + private def derive_class_name class_name = name.to_s.camelize @@ -363,6 +379,10 @@ module ActiveRecord end end + def derive_join_table + [active_record.table_name, klass.table_name].sort.join("\0").gsub(/^(.*_)(.+)\0\1(.+)/, '\1\2_\3').gsub("\0", "_") + end + def primary_key(klass) klass.primary_key || raise(UnknownPrimaryKey.new(klass)) end @@ -431,29 +451,25 @@ module ActiveRecord # has_many :tags # end # - # There may be conditions on Person.comment_tags, Article.comment_tags and/or Comment.tags, + # There may be scopes 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.map { |c| c.dup } + # of scopes corresponding to the chain. + def scope_chain + @scope_chain ||= begin + scope_chain = source_reflection.scope_chain.map(&:dup) - # Add to it the conditions from this reflection if necessary. - conditions.first << options[:conditions] if options[:conditions] + # Add to it the scope from this reflection (if any) + scope_chain.first << scope if scope - through_conditions = through_reflection.conditions + through_scope_chain = through_reflection.scope_chain if options[:source_type] - through_conditions.first << { foreign_type => options[:source_type] } + through_scope_chain.first << + through_reflection.klass.where(foreign_type => options[:source_type]) end # Recursively fill out the rest of the array from the through reflection - conditions += through_conditions - - # And return - conditions + scope_chain + through_scope_chain end end diff --git a/activerecord/lib/active_record/relation.rb b/activerecord/lib/active_record/relation.rb index 3d1ca4f99b..ed80422336 100644 --- a/activerecord/lib/active_record/relation.rb +++ b/activerecord/lib/active_record/relation.rb @@ -1,33 +1,34 @@ # -*- coding: utf-8 -*- -require 'active_support/core_ext/object/blank' -require 'active_support/deprecation' module ActiveRecord # = Active Record Relation class Relation JoinOperation = Struct.new(:relation, :join_class, :on) - ASSOCIATION_METHODS = [:includes, :eager_load, :preload] - MULTI_VALUE_METHODS = [:select, :group, :order, :joins, :where, :having, :bind, :references] - SINGLE_VALUE_METHODS = [:limit, :offset, :lock, :readonly, :from, :reordering, :reverse_order, :uniq] + + MULTI_VALUE_METHODS = [:includes, :eager_load, :preload, :select, :group, + :order, :joins, :where, :having, :bind, :references, + :extending] + + SINGLE_VALUE_METHODS = [:limit, :offset, :lock, :readonly, :from, :reordering, + :reverse_order, :uniq, :create_with] + + VALUE_METHODS = MULTI_VALUE_METHODS + SINGLE_VALUE_METHODS include FinderMethods, Calculations, SpawnMethods, QueryMethods, Batches, Explain, Delegation attr_reader :table, :klass, :loaded - attr_accessor :extensions, :default_scoped + attr_accessor :default_scoped + alias :model :klass alias :loaded? :loaded alias :default_scoped? :default_scoped - def initialize(klass, table) - @klass, @table = klass, table - + def initialize(klass, table, values = {}) + @klass = klass + @table = table + @values = values @implicit_readonly = nil @loaded = false @default_scoped = false - - SINGLE_VALUE_METHODS.each {|v| instance_variable_set(:"@#{v}_value", nil)} - (ASSOCIATION_METHODS + MULTI_VALUE_METHODS).each {|v| instance_variable_set(:"@#{v}_values", [])} - @extensions = [] - @create_with_value = {} end def insert(values) @@ -73,28 +74,62 @@ module ActiveRecord binds) end + # Initializes new record from relation while maintaining the current + # scope. + # + # Expects arguments in the same format as +Base.new+. + # + # users = User.where(name: 'DHH') + # user = users.new # => #<User id: nil, name: "DHH", created_at: nil, updated_at: nil> + # + # You can also pass a block to new with the new record as argument: + # + # user = users.new { |user| user.name = 'Oscar' } + # user.name # => Oscar def new(*args, &block) scoping { @klass.new(*args, &block) } end def initialize_copy(other) - @bind_values = @bind_values.dup + @values = @values.dup + @values[:bind] = @values[:bind].dup if @values[:bind] reset end alias build new + # Tries to create a new record with the same scoped attributes + # defined in the relation. Returns the initialized object if validation fails. + # + # Expects arguments in the same format as +Base.create+. + # + # ==== Examples + # users = User.where(name: 'Oscar') + # users.create # #<User id: 3, name: "oscar", ...> + # + # users.create(name: 'fxn') + # users.create # #<User id: 4, name: "fxn", ...> + # + # users.create { |user| user.name = 'tenderlove' } + # # #<User id: 5, name: "tenderlove", ...> + # + # users.create(name: nil) # validation on name + # # #<User id: nil, name: nil, ...> def create(*args, &block) scoping { @klass.create(*args, &block) } end + # Similar to #create, but calls +create!+ on the base class. Raises + # an exception if a validation error occurs. + # + # Expects arguments in the same format as <tt>Base.create!</tt>. def create!(*args, &block) scoping { @klass.create!(*args, &block) } end # Tries to load the first record; if it fails, then <tt>create</tt> is called with the same arguments as this method. # - # Expects arguments in the same format as <tt>Base.create</tt>. + # Expects arguments in the same format as +Base.create+. # # ==== Examples # # Find the first user named Penélope or create a new one. @@ -116,22 +151,22 @@ module ActiveRecord # user.last_name = "O'Hara" # end # # => <User id: 2, first_name: 'Scarlett', last_name: 'Johansson'> - def first_or_create(attributes = nil, options = {}, &block) - first || create(attributes, options, &block) + def first_or_create(attributes = nil, &block) + first || create(attributes, &block) end # Like <tt>first_or_create</tt> but calls <tt>create!</tt> so an exception is raised if the created record is invalid. # # Expects arguments in the same format as <tt>Base.create!</tt>. - def first_or_create!(attributes = nil, options = {}, &block) - first || create!(attributes, options, &block) + def first_or_create!(attributes = nil, &block) + first || create!(attributes, &block) end # Like <tt>first_or_create</tt> but calls <tt>new</tt> instead of <tt>create</tt>. # # Expects arguments in the same format as <tt>Base.new</tt>. - def first_or_initialize(attributes = nil, options = {}, &block) - first || new(attributes, options, &block) + def first_or_initialize(attributes = nil, &block) + first || new(attributes, &block) end # Runs EXPLAIN on the query or queries triggered by this relation and @@ -142,58 +177,17 @@ module ActiveRecord # are needed by the next ones when eager loading is going on. # # Please see further details in the - # {Active Record Query Interface guide}[http://edgeguides.rubyonrails.org/active_record_querying.html#running-explain]. + # {Active Record Query Interface guide}[http://guides.rubyonrails.org/active_record_querying.html#running-explain]. def explain _, queries = collecting_queries_for_explain { exec_queries } exec_explain(queries) end + # Converts relation objects to Array. def to_a - # We monitor here the entire execution rather than individual SELECTs - # because from the point of view of the user fetching the records of a - # relation is a single unit of work. You want to know if this call takes - # too long, not if the individual queries take too long. - # - # It could be the case that none of the queries involved surpass the - # threshold, and at the same time the sum of them all does. The user - # should get a query plan logged in that case. - logging_query_plan do - exec_queries - end - end - - def exec_queries - return @records if loaded? - - default_scoped = with_default_scope - - if default_scoped.equal?(self) - @records = if @readonly_value.nil? && !@klass.locking_enabled? - eager_loading? ? find_with_associations : @klass.find_by_sql(arel, @bind_values) - else - IdentityMap.without do - eager_loading? ? find_with_associations : @klass.find_by_sql(arel, @bind_values) - end - end - - preload = @preload_values - preload += @includes_values unless eager_loading? - 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 - # are JOINS and no explicit SELECT. - readonly = @readonly_value.nil? ? @implicit_readonly : @readonly_value - @records.each { |record| record.readonly! } if readonly - else - @records = default_scoped.to_a - end - - @loaded = true + load @records end - private :exec_queries def as_json(options = nil) #:nodoc: to_a.as_json(options) @@ -212,6 +206,7 @@ module ActiveRecord c.respond_to?(:zero?) ? c.zero? : c.empty? end + # Returns true if there are any records. def any? if block_given? to_a.any? { |*block_args| yield(*block_args) } @@ -220,18 +215,17 @@ module ActiveRecord end end + # Returns true if there is more than one record. def many? if block_given? to_a.many? { |*block_args| yield(*block_args) } else - @limit_value ? to_a.many? : size > 1 + limit_value ? to_a.many? : size > 1 end end # Scope all queries to the current scope. # - # ==== Example - # # Comment.where(:post_id => 1).scoping do # Comment.first # SELECT * FROM comments WHERE post_id = 1 # end @@ -239,7 +233,10 @@ module ActiveRecord # Please check unscoped if you want to remove all previous scopes (including # the default_scope) during the execution of a block. def scoping - @klass.with_scope(self, :overwrite) { yield } + previous, klass.current_scope = klass.current_scope, self + yield + ensure + klass.current_scope = previous end # Updates all records with details given if they match a set of conditions supplied, limits and order can @@ -250,50 +247,35 @@ 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. - # See conditions in the intro. - # * +options+ - Additional options are <tt>:limit</tt> and <tt>:order</tt>, see the examples for usage. # # ==== Examples # # # Update all customers with the given attributes - # Customer.update_all :wants_email => true + # Customer.update_all wants_email: true # # # Update all books with 'Rails' in their title - # Book.update_all "author = 'David'", "title LIKE '%Rails%'" - # - # # Update all avatars migrated more than a week ago - # Avatar.update_all ['migrated_at = ?', Time.now.utc], ['migrated_at > ?', 1.week.ago] + # Book.where('title LIKE ?', '%Rails%').update_all(author: 'David') # # # 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 = {}) - IdentityMap.repository[symbolized_base_class].clear if IdentityMap.enabled? - if conditions || options.present? - where(conditions).apply_finder_options(options.slice(:limit, :order)).update_all(updates) - else - stmt = Arel::UpdateManager.new(arel.engine) + def update_all(updates) + raise ArgumentError, "Empty list of attributes to change" if updates.blank? - stmt.set Arel.sql(@klass.send(:sanitize_sql_for_assignment, updates)) - stmt.table(table) - stmt.key = table[primary_key] + stmt = Arel::UpdateManager.new(arel.engine) - if joins_values.any? - @klass.connection.join_to_update(stmt, arel) - else - stmt.take(arel.limit) - stmt.order(*arel.orders) - stmt.wheres = arel.constraints - end + stmt.set Arel.sql(@klass.send(:sanitize_sql_for_assignment, updates)) + stmt.table(table) + stmt.key = table[primary_key] - @klass.connection.update stmt, 'SQL', bind_values + if joins_values.any? + @klass.connection.join_to_update(stmt, arel) + else + stmt.take(arel.limit) + stmt.order(*arel.orders) + stmt.wheres = arel.constraints end + + @klass.connection.update stmt, 'SQL', bind_values end # Updates an object (or multiple objects) and saves it to the database, if validations pass. @@ -307,14 +289,14 @@ module ActiveRecord # ==== Examples # # # Updates one record - # Person.update(15, :user_name => 'Samuel', :group => 'expert') + # Person.update(15, user_name: 'Samuel', group: 'expert') # # # Updates multiple records # people = { 1 => { "first_name" => "David" }, 2 => { "first_name" => "Jeremy" } } # Person.update(people.keys, people.values) def update(id, attributes) if id.is_a?(Array) - id.each.with_index.map {|one_id, idx| update(one_id, attributes[idx])} + id.map.with_index { |one_id, idx| update(one_id, attributes[idx]) } else object = find(id) object.update_attributes(attributes) @@ -347,7 +329,7 @@ module ActiveRecord # ==== Examples # # Person.destroy_all("last_login < '2004-04-04'") - # Person.destroy_all(:status => "inactive") + # Person.destroy_all(status: "inactive") # Person.where(:age => 0..18).destroy_all def destroy_all(conditions = nil) if conditions @@ -384,17 +366,12 @@ module ActiveRecord end end - # Deletes the records matching +conditions+ without instantiating the records first, and hence not - # calling the +destroy+ method nor invoking callbacks. This is a single SQL DELETE statement that - # goes straight to the database, much more efficient than +destroy_all+. Be careful with relations - # though, in particular <tt>:dependent</tt> rules defined on associations are not honored. Returns - # the number of rows affected. - # - # ==== Parameters - # - # * +conditions+ - Conditions are specified the same way as with +find+ method. - # - # ==== Example + # Deletes the records matching +conditions+ without instantiating the records + # first, and hence not calling the +destroy+ method nor invoking callbacks. This + # is a single SQL DELETE statement that goes straight to the database, much more + # efficient than +destroy_all+. Be careful with relations though, in particular + # <tt>:dependent</tt> rules defined on associations are not honored. Returns the + # number of rows affected. # # Post.delete_all("person_id = 5 AND (category = 'Something' OR category = 'Else')") # Post.delete_all(["person_id = ? AND (category = ? OR category = ?)", 5, 'Something', 'Else']) @@ -403,13 +380,27 @@ module ActiveRecord # 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. + # + # If a limit scope is supplied, +delete_all+ raises an ActiveRecord error: + # + # Post.limit(100).delete_all + # # => ActiveRecord::ActiveRecordError: delete_all doesn't support limit scope def delete_all(conditions = nil) - IdentityMap.repository[symbolized_base_class] = {} if IdentityMap.enabled? + raise ActiveRecordError.new("delete_all doesn't support limit scope") if self.limit_value + if conditions where(conditions).delete_all else - statement = arel.compile_delete - affected = @klass.connection.delete(statement, 'SQL', bind_values) + stmt = Arel::DeleteManager.new(arel.engine) + stmt.from(table) + + if joins_values.any? + @klass.connection.join_to_delete(stmt, arel, table[primary_key]) + else + stmt.wheres = arel.constraints + end + + affected = @klass.connection.delete(stmt, 'SQL', bind_values) reset affected @@ -437,14 +428,35 @@ module ActiveRecord # # Delete multiple rows # Todo.delete([2,3,4]) def delete(id_or_array) - IdentityMap.remove_by_id(self.symbolized_base_class, id_or_array) if IdentityMap.enabled? where(primary_key => id_or_array).delete_all end + # Causes the records to be loaded from the database if they have not + # been loaded already. You can use this if for some reason you need + # to explicitly load some records before actually using them. The + # return value is the relation itself, not the records. + # + # Post.where(published: true).load # => #<ActiveRecord::Relation> + def load + unless loaded? + # We monitor here the entire execution rather than individual SELECTs + # because from the point of view of the user fetching the records of a + # relation is a single unit of work. You want to know if this call takes + # too long, not if the individual queries take too long. + # + # It could be the case that none of the queries involved surpass the + # threshold, and at the same time the sum of them all does. The user + # should get a query plan logged in that case. + logging_query_plan { exec_queries } + end + + self + end + + # Forces reloading of relation. def reload reset - to_a # force reload - self + load end def reset @@ -454,26 +466,40 @@ module ActiveRecord self end + # Returns sql statement for the relation. + # + # Users.where(name: 'Oscar').to_sql + # # => SELECT "users".* FROM "users" WHERE "users"."name" = 'Oscar' def to_sql - @to_sql ||= klass.connection.to_sql(arel, @bind_values.dup) + @to_sql ||= klass.connection.to_sql(arel, bind_values.dup) end + # Returns a hash of where conditions + # + # Users.where(name: 'Oscar').where_values_hash + # # => {:name=>"oscar"} def where_values_hash equalities = with_default_scope.where_values.grep(Arel::Nodes::Equality).find_all { |node| node.left.relation.name == table_name } - Hash[equalities.map { |where| [where.left.name, where.right] }] + binds = Hash[bind_values.find_all(&:first).map { |column, v| [column.name, v] }] + + Hash[equalities.map { |where| + name = where.left.name + [name, binds.fetch(name.to_s) { where.right }] + }] end def scope_for_create @scope_for_create ||= where_values_hash.merge(create_with_value) end + # Returns true if relation needs eager loading. def eager_loading? @should_eager_load ||= - @eager_load_values.any? || - @includes_values.any? && (joined_includes_values.any? || references_eager_loaded_tables?) + eager_load_values.any? || + includes_values.any? && (joined_includes_values.any? || references_eager_loaded_tables?) end # Joins that are also marked for preloading. In which case we should just eager load them. @@ -481,9 +507,10 @@ module ActiveRecord # represent the same association, but that aren't matched by this. Also, we could have # nested hashes which partially match, e.g. { :a => :b } & { :a => [:b, :c] } def joined_includes_values - @includes_values & @joins_values + includes_values & joins_values end + # Compares two relations for equality. def ==(other) case other when Relation @@ -493,10 +520,6 @@ module ActiveRecord end end - def inspect - to_a.inspect - end - def pretty_print(q) q.pp(self.to_a) end @@ -511,8 +534,48 @@ module ActiveRecord end end + # Returns true if relation is blank. + def blank? + to_a.blank? + end + + def values + @values.dup + end + + def inspect + entries = to_a.take([limit_value, 11].compact.min).map!(&:inspect) + entries[10] = '...' if entries.size == 11 + + "#<#{self.class.name} [#{entries.join(', ')}]>" + end + private + def exec_queries + default_scoped = with_default_scope + + if default_scoped.equal?(self) + @records = eager_loading? ? find_with_associations : @klass.find_by_sql(arel, bind_values) + + preload = preload_values + preload += includes_values unless eager_loading? + 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 + # are JOINS and no explicit SELECT. + readonly = readonly_value.nil? ? @implicit_readonly : readonly_value + @records.each { |record| record.readonly! } if readonly + else + @records = default_scoped.to_a + end + + @loaded = true + @records + end + def references_eager_loaded_tables? joined_tables = arel.join_sources.map do |join| if join.is_a?(Arel::Nodes::StringJoin) diff --git a/activerecord/lib/active_record/relation/batches.rb b/activerecord/lib/active_record/relation/batches.rb index 2fd89882ff..d32048cce1 100644 --- a/activerecord/lib/active_record/relation/batches.rb +++ b/activerecord/lib/active_record/relation/batches.rb @@ -1,21 +1,26 @@ -require 'active_support/core_ext/object/blank' module ActiveRecord 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). + # Looping through a collection of records from the database + # (using the +all+ method, for example) is very inefficient + # since it will try to instantiate all the objects at once. # - # Example: + # In that case, batch processing methods allow you to work + # with the records in batches, thereby greatly reducing memory consumption. + # + # The #find_each method uses #find_in_batches with a batch size of 1000 (or as + # specified by the +:batch_size+ option). + # + # Person.all.find_each do |person| + # person.do_awesome_stuff + # end # # Person.where("age > 21").find_each do |person| # person.party_all_night! # end # - # Note: This method is only intended to use for batch processing of - # large amounts of records that wouldn't fit in memory all at once. If - # you just need to loop over less than 1000 records, it's probably - # better just to use the regular find methods. + # You can also pass the +:start+ option to specify + # an offset to control the starting point. def find_each(options = {}) find_in_batches(options) do |records| records.each { |record| yield record } @@ -23,51 +28,50 @@ module ActiveRecord end # Yields each batch of records that was found by the find +options+ as - # an array. The size of each batch is set by the <tt>:batch_size</tt> + # an array. The size of each batch is set by the +:batch_size+ # option; the default is 1000. # # You can control the starting point for the batch processing by - # supplying the <tt>:start</tt> option. This is especially useful if you + # supplying the +:start+ option. This is especially useful if you # want multiple workers dealing with the same processing queue. You can # make worker 1 handle all the records between id 0 and 10,000 and - # worker 2 handle from 10,000 and beyond (by setting the <tt>:start</tt> - # option on that worker). + # worker 2 handle from 10,000 and beyond (by setting the +:start+ + # option on that worker). You can also use non-integer-based primary keys + # if start point is set. # # It's not possible to set the order. That is automatically set to - # ascending on the primary key ("id ASC") to make the batch ordering - # work. This also mean that this method only works with integer-based - # primary keys. You can't set the limit either, that's used to control + # ascending on the primary key (e.g. "id ASC") to make the batch ordering + # work. You can't set the limit either, that's used to control # the batch sizes. # - # Example: - # # Person.where("age > 21").find_in_batches do |group| # sleep(50) # Make sure it doesn't get too crowded in there! # group.each { |person| person.party_all_night! } # end + # + # # Let's process the next 2000 records + # Person.all.find_in_batches(start: 2000, batch_size: 2000) do |group| + # group.each { |person| person.party_all_night! } + # end def find_in_batches(options = {}) + options.assert_valid_keys(:start, :batch_size) + relation = self 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 - if (finder_options = options.except(:start, :batch_size)).present? - raise "You can't specify an order, it's forced to be #{batch_order}" if options[:order].present? - raise "You can't specify a limit, it's forced to be the batch_size" if options[:limit].present? - - relation = apply_finder_options(finder_options) - end - - start = options.delete(:start).to_i + start = options.delete(:start) + start ||= 0 batch_size = options.delete(:batch_size) || 1000 relation = relation.reorder(batch_order).limit(batch_size) - records = relation.where(table[primary_key].gteq(start)).all + records = relation.where(table[primary_key].gteq(start)).to_a while records.any? records_size = records.size - primary_key_offset = records.last.id + primary_key_offset = records.last.send(primary_key) yield records diff --git a/activerecord/lib/active_record/relation/calculations.rb b/activerecord/lib/active_record/relation/calculations.rb index 63365e501b..7c43d844d0 100644 --- a/activerecord/lib/active_record/relation/calculations.rb +++ b/activerecord/lib/active_record/relation/calculations.rb @@ -1,61 +1,30 @@ -require 'active_support/core_ext/object/blank' require 'active_support/core_ext/object/try' module ActiveRecord module Calculations - # 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 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 ]. - # 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. - # 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 - # 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 - # SELECT COUNT(DISTINCT posts.id) ... - # * <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: - # Person.count # returns the total count of all people - # - # Examples for counting by column: - # Person.count(:age) # returns the total count of all people whose age is present in database - # - # Examples for count with options: - # 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) - # - # # 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('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. - # Use Person.count instead. + # Count the records. + # + # Person.count + # # => the total count of all people + # + # Person.count(:age) + # # => returns the total count of all people whose age is present in database + # + # Person.count(:all) + # # => performs a COUNT(*) (:all is an alias for '*') + # + # Person.count(:age, distinct: true) + # # => counts the number of different age values + # + # Person.where("age > 26").count { |person| person.gender == 'female' } + # # => queries people where "age > 26" then count the loaded results filtering by gender def count(column_name = nil, options = {}) - column_name, options = nil, column_name if column_name.is_a?(Hash) - calculate(:count, column_name, options) + if block_given? + self.to_a.count { |item| yield item } + else + column_name, options = nil, column_name if column_name.is_a?(Hash) + calculate(:count, column_name, options) + end end # Calculates the average value on a given column. Returns +nil+ if there's @@ -89,30 +58,35 @@ module ActiveRecord # +calculate+ for examples with options. # # Person.sum('age') # => 4562 + # # => returns the total sum of all people's age + # + # Person.where('age > 100').sum { |person| person.age - 100 } + # # queries people where "age > 100" then perform a sum calculation with the block returns def sum(*args) if block_given? - self.to_a.sum(*args) {|*block_args| yield(*block_args)} + self.to_a.sum(*args) { |item| yield item } else calculate(:sum, *args) end 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>, - # <tt>:order</tt>, <tt>:group</tt>, <tt>:having</tt>, and <tt>:joins</tt> can be passed to customize the query. + # minimum, and maximum have been added as shortcuts. # # There are two basic forms of output: + # # * 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 - # <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') + # * Grouped values: This returns an ordered hash of the values and groups them. It + # takes either a column name, or the name of a belongs_to association. + # + # values = Person.group('last_name').maximum(:age) # puts values["Drake"] # => 43 # # drake = Family.find_by_last_name('Drake') - # values = Person.maximum(:age, :group => :family) # Person belongs_to :family + # values = Person.group(:family).maximum(:age) # Person belongs_to :family # puts values[drake] # => 43 # @@ -120,85 +94,113 @@ module ActiveRecord # ... # end # - # Options: - # * <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, - # 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". - # (Rarely needed). - # 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 - # 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) ... - # # 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 - # # 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.group(:last_name).having("min(age) > 17").minimum(:age) # # Person.sum("2 * age") def calculate(operation, column_name, options = {}) - if options.except(:distinct).present? - apply_finder_options(options.except(:distinct)).calculate(operation, column_name, :distinct => options[:distinct]) - else - relation = with_default_scope + relation = with_default_scope - if relation.equal?(self) - if eager_loading? || (includes_values.present? && references_eager_loaded_tables?) - construct_relation_for_association_calculations.calculate(operation, column_name, options) - else - perform_calculation(operation, column_name, options) - end + if relation.equal?(self) + if has_include?(column_name) + construct_relation_for_association_calculations.calculate(operation, column_name, options) else - relation.calculate(operation, column_name, options) + perform_calculation(operation, column_name, options) end + else + relation.calculate(operation, column_name, options) end rescue ThrowResult 0 end - # This method is designed to perform select by a single column as direct SQL query - # Returns <tt>Array</tt> with values of the specified column name - # The values has same data type as column. + # Use <tt>pluck</tt> as a shortcut to select a single attribute without + # loading a bunch of records just to grab one attribute you want. + # + # Person.pluck(:name) + # + # instead of + # + # Person.all.map(&:name) + # + # Pluck returns an <tt>Array</tt> of attribute values type-casted to match + # the plucked column name, if it can be deduced. Plucking an SQL fragment + # returns String values by default. # # Examples: # - # Person.pluck(:id) # SELECT people.id FROM people - # Person.uniq.pluck(:role) # SELECT DISTINCT role FROM people - # Person.where(:confirmed => true).limit(5).pluck(:id) + # Person.pluck(:id) + # # SELECT people.id FROM people + # # => [1, 2, 3] # - def pluck(column_name) - key = column_name.to_s.split('.', 2).last - - if column_name.is_a?(Symbol) && column_names.include?(column_name.to_s) - column_name = "#{table_name}.#{column_name}" + # Person.pluck(:id, :name) + # # SELECT people.id, people.name FROM people + # # => [[1, 'David'], [2, 'Jeremy'], [3, 'Jose']] + # + # Person.uniq.pluck(:role) + # # SELECT DISTINCT role FROM people + # # => ['admin', 'member', 'guest'] + # + # Person.where(:age => 21).limit(5).pluck(:id) + # # SELECT people.id FROM people WHERE people.age = 21 LIMIT 5 + # # => [2, 3] + # + # Person.pluck('DATEDIFF(updated_at, created_at)') + # # SELECT DATEDIFF(updated_at, created_at) FROM people + # # => ['0', '27761', '173'] + # + def pluck(*column_names) + column_names.map! do |column_name| + if column_name.is_a?(Symbol) && self.column_names.include?(column_name.to_s) + "#{connection.quote_table_name(table_name)}.#{connection.quote_column_name(column_name)}" + else + column_name + end end - result = klass.connection.select_all(select(column_name).arel) - types = result.column_types.merge klass.column_types - column = types[key] + if has_include?(column_names.first) + construct_relation_for_association_calculations.pluck(*column_names) + else + result = klass.connection.select_all(select(column_names).arel, nil, bind_values) + columns = result.columns.map do |key| + klass.column_types.fetch(key) { + result.column_types.fetch(key) { + Class.new { def type_cast(v); v; end }.new + } + } + end - result.map do |attributes| - value = klass.initialize_attributes(attributes)[key] - if column - column.type_cast value - else - value + result = result.map do |attributes| + values = klass.initialize_attributes(attributes).values + + columns.zip(values).map do |column, value| + column.type_cast(value) + end end + columns.one? ? result.map!(&:first) : result end end + # Pluck all the ID's for the relation using the table's primary key + # + # Examples: + # + # Person.ids # SELECT people.id FROM people + # Person.joins(:companies).ids # SELECT people.id FROM people INNER JOIN companies ON companies.person_id = people.id + def ids + pluck primary_key + end + private + def has_include?(column_name) + eager_loading? || (includes_values.present? && (column_name || references_eager_loaded_tables?)) + end + def perform_calculation(operation, column_name, options = {}) operation = operation.to_s.downcase @@ -216,7 +218,7 @@ module ActiveRecord distinct = nil if column_name =~ /\s*DISTINCT\s+/i end - if @group_values.any? + if group_values.any? execute_grouped_calculation(operation, column_name, distinct) else execute_simple_calculation(operation, column_name, distinct) @@ -254,14 +256,21 @@ module ActiveRecord query_builder = relation.arel end - type_cast_calculated_value(@klass.connection.select_value(query_builder), column_for(column_name), operation) + result = @klass.connection.select_value(query_builder, nil, relation.bind_values) + type_cast_calculated_value(result, 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_attrs = group_values + + if group_attrs.first.respond_to?(:to_sym) + association = @klass.reflect_on_association(group_attrs.first.to_sym) + associated = group_attrs.size == 1 && association && association.macro == :belongs_to # only count belongs_to associations + group_fields = Array(associated ? association.foreign_key : group_attrs) + else + group_fields = group_attrs + end + group_aliases = group_fields.map { |field| column_alias_for(field) } group_columns = group_aliases.zip(group_fields).map { |aliaz,field| [aliaz, column_for(field)] @@ -281,16 +290,20 @@ module ActiveRecord operation, distinct).as(aggregate_alias) ] - select_values += @select_values unless @having_values.empty? + select_values += select_values unless having_values.empty? select_values.concat group_fields.zip(group_aliases).map { |field,aliaz| - "#{field} AS #{aliaz}" + if field.respond_to?(:as) + field.as(aliaz) + else + "#{field} AS #{aliaz}" + end } - relation = except(:group).group(group.join(',')) + relation = except(:group).group(group) relation.select_values = select_values - calculated_data = @klass.connection.select_all(relation) + calculated_data = @klass.connection.select_all(relation, nil, bind_values) if association key_ids = calculated_data.collect { |row| row[group_aliases.first] } @@ -298,11 +311,11 @@ module ActiveRecord key_records = Hash[key_records.map { |r| [r.id, r] }] end - ActiveSupport::OrderedHash[calculated_data.map do |row| - key = group_columns.map { |aliaz, column| + Hash[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.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] @@ -317,6 +330,7 @@ module ActiveRecord # column_alias_for("count(*)") # => "count_all" # column_alias_for("count", "id") # => "count_id" def column_alias_for(*keys) + keys.map! {|k| k.respond_to?(:to_sql) ? k.to_sql : k} table_name = keys.join(' ') table_name.downcase! table_name.gsub!(/\*/, 'all') @@ -328,7 +342,7 @@ module ActiveRecord end def column_for(field) - field_name = field.to_s.split('.').last + field_name = field.respond_to?(:name) ? field.name.to_s : field.to_s.split('.').last @klass.columns.detect { |c| c.name.to_s == field_name } end @@ -346,8 +360,8 @@ module ActiveRecord end def select_for_count - if @select_values.present? - select = @select_values.join(", ") + if select_values.present? + select = select_values.join(", ") select if select !~ /[,*]/ end end diff --git a/activerecord/lib/active_record/relation/delegation.rb b/activerecord/lib/active_record/relation/delegation.rb index f5fdf437bf..ab8b36c8ab 100644 --- a/activerecord/lib/active_record/relation/delegation.rb +++ b/activerecord/lib/active_record/relation/delegation.rb @@ -1,7 +1,6 @@ -require 'active_support/core_ext/module/delegation' module ActiveRecord - module Delegation + module Delegation # :nodoc: # Set up common delegations for performance (avoids method_missing) delegate :to_xml, :to_yaml, :length, :collect, :map, :each, :all?, :include?, :to_ary, :to => :to_a delegate :table_name, :quoted_table_name, :primary_key, :quoted_primary_key, @@ -32,12 +31,12 @@ module ActiveRecord protected def method_missing(method, *args, &block) - if Array.method_defined?(method) - ::ActiveRecord::Delegation.delegate method, :to => :to_a - to_a.send(method, *args, &block) - elsif @klass.respond_to?(method) + if @klass.respond_to?(method) ::ActiveRecord::Delegation.delegate_to_scoped_klass(method) scoping { @klass.send(method, *args, &block) } + elsif Array.method_defined?(method) + ::ActiveRecord::Delegation.delegate method, :to => :to_a + to_a.send(method, *args, &block) elsif arel.respond_to?(method) ::ActiveRecord::Delegation.delegate method, :to => :arel arel.send(method, *args, &block) @@ -46,4 +45,4 @@ module ActiveRecord end end end -end
\ No newline at end of file +end diff --git a/activerecord/lib/active_record/relation/finder_methods.rb b/activerecord/lib/active_record/relation/finder_methods.rb index 26a5165abf..84aaa39fed 100644 --- a/activerecord/lib/active_record/relation/finder_methods.rb +++ b/activerecord/lib/active_record/relation/finder_methods.rb @@ -1,85 +1,23 @@ -require 'active_support/core_ext/object/blank' require 'active_support/core_ext/hash/indifferent_access' module ActiveRecord module FinderMethods - # Find operates with four different retrieval approaches: + # Find by id - This can either be a specific id (1), a list of ids (1, 5, 6), or an array of ids ([5, 6, 10]). + # If no record can be found for all of the listed ids, then RecordNotFound will be raised. If the primary key + # is an integer, find by id coerces its arguments using +to_i+. # - # * Find by id - This can either be a specific id (1), a list of ids (1, 5, 6), or an array of ids ([5, 6, 10]). - # If no record can be found for all of the listed ids, then RecordNotFound will be raised. - # * Find first - This will return the first record matched by the options used. These options can either be specific - # conditions or merely an order. If no record can be matched, +nil+ is returned. Use - # <tt>Model.find(:first, *args)</tt> or its shortcut <tt>Model.first(*args)</tt>. - # * Find last - This will return the last record matched by the options used. These options can either be specific - # conditions or merely an order. If no record can be matched, +nil+ is returned. Use - # <tt>Model.find(:last, *args)</tt> or its shortcut <tt>Model.last(*args)</tt>. - # * Find all - This will return all the records matched by the options used. - # If no records are found, an empty array is returned. Use - # <tt>Model.find(:all, *args)</tt> or its shortcut <tt>Model.all(*args)</tt>. - # - # All approaches accept an options hash as their last parameter. - # - # ==== Options - # - # * <tt>:conditions</tt> - An SQL fragment like "administrator = 1", <tt>["user_name = ?", username]</tt>, - # or <tt>["user_name = :user_name", { :user_name => user_name }]</tt>. See conditions in the intro. - # * <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>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, - # 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 - # <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 - # 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, - # 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 - # 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". - # <tt>:lock => true</tt> gives connection's default exclusive lock, usually "FOR UPDATE". - # - # ==== Examples - # - # # find by id # Person.find(1) # returns the object for ID = 1 + # Person.find("1") # returns the object for ID = 1 # 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.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> + # provide since database rows are unordered. Give an explicit <tt>order</tt> # to ensure the results are sorted. # - # ==== Examples - # - # # find first - # 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.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.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 + # ==== Find with lock # # 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 @@ -93,30 +31,62 @@ module ActiveRecord # person.save! # end def find(*args) - return to_a.find { |*block_args| yield(*block_args) } if block_given? - - options = args.extract_options! - - if options.present? - apply_finder_options(options).find(*args) + if block_given? + to_a.find { |*block_args| yield(*block_args) } else - case args.first - when :first, :last, :all - send(args.first) - else - find_with_ids(*args) - end + find_with_ids(*args) end end - # A convenience wrapper for <tt>find(:first, *args)</tt>. You can pass in all the - # same arguments to this method as you can to <tt>find(:first)</tt>. - def first(*args) - if args.any? - if args.first.kind_of?(Integer) || (loaded? && !args.first.kind_of?(Hash)) - limit(*args).to_a + # Finds the first record matching the specified conditions. There + # is no implied ording so if order matters, you should specify it + # yourself. + # + # If no record is found, returns <tt>nil</tt>. + # + # Post.find_by name: 'Spartacus', rating: 4 + # Post.find_by "published_at < ?", 2.weeks.ago + def find_by(*args) + where(*args).take + end + + # Like <tt>find_by</tt>, except that if no record is found, raises + # an <tt>ActiveRecord::RecordNotFound</tt> error. + def find_by!(*args) + where(*args).take! + end + + # Gives a record (or N records if a parameter is supplied) without any implied + # order. The order will depend on the database implementation. + # If an order is supplied it will be respected. + # + # Person.take # returns an object fetched by SELECT * FROM people + # Person.take(5) # returns 5 objects fetched by SELECT * FROM people LIMIT 5 + # Person.where(["name LIKE '%?'", name]).take + def take(limit = nil) + limit ? limit(limit).to_a : find_take + end + + # Same as +take+ but raises <tt>ActiveRecord::RecordNotFound</tt> if no record + # is found. Note that <tt>take!</tt> accepts no arguments. + def take! + take or raise RecordNotFound + end + + # Find the first record (or first N records if a parameter is supplied). + # If no order is defined it will order by primary key. + # + # 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 + # Person.first(3) # returns the first three objects fetched by SELECT * FROM people LIMIT 3 + def first(limit = nil) + if limit + if order_values.empty? && primary_key + order(arel_table[primary_key].asc).limit(limit).to_a else - apply_finder_options(args.first).first + limit(limit).to_a end else find_first @@ -129,18 +99,27 @@ module ActiveRecord first 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) - if args.any? - if args.first.kind_of?(Integer) || (loaded? && !args.first.kind_of?(Hash)) - if order_values.empty? - order("#{primary_key} DESC").limit(*args).reverse - else - to_a.last(*args) - end + # Find the last record (or last N records if a parameter is supplied). + # If no order is defined it will order by primary key. + # + # 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 + # Person.last(3) # returns the last three objects fetched by SELECT * FROM people. + # + # Take note that in that last case, the results are sorted in ascending order: + # + # [#<Person id:2>, #<Person id:3>, #<Person id:4>] + # + # and not: + # + # [#<Person id:4>, #<Person id:3>, #<Person id:2>] + def last(limit = nil) + if limit + if order_values.empty? && primary_key + order(arel_table[primary_key].desc).limit(limit).reverse else - apply_finder_options(args.first).last + to_a.last(limit) end else find_last @@ -153,14 +132,8 @@ module ActiveRecord last 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) - args.any? ? apply_finder_options(args.first).to_a : to_a - end - - # Returns true if a record exists in the table that matches the +id+ or - # conditions given, or false otherwise. The argument can take five forms: + # Returns +true+ if a record exists in the table that matches the +id+ or + # conditions given, or +false+ otherwise. The argument can take six forms: # # * Integer - Finds the record with this primary key. # * String - Finds the record with a primary key corresponding to this @@ -168,8 +141,9 @@ module ActiveRecord # * Array - Finds the record that matches these +find+-style conditions # (such as <tt>['color = ?', 'red']</tt>). # * Hash - Finds the record that matches these +find+-style conditions - # (such as <tt>{:color => 'red'}</tt>). - # * No args - Returns false if the table is empty, true otherwise. + # (such as <tt>{color: 'red'}</tt>). + # * +false+ - Returns always +false+. + # * No args - Returns +false+ if the table is empty, +true+ otherwise. # # For more information about specifying conditions as a Hash or Array, # see the Conditions section in the introduction to ActiveRecord::Base. @@ -178,29 +152,30 @@ module ActiveRecord # 'Jamie'</tt>), since it would be sanitized and then queried against # the primary key column, like <tt>id = 'name = \'Jamie\''</tt>. # - # ==== Examples # Person.exists?(5) # Person.exists?('5') - # Person.exists?(:name => "David") # Person.exists?(['name LIKE ?', "%#{query}%"]) + # Person.exists?(name: 'David') + # Person.exists?(false) # Person.exists? - def exists?(id = false) - return false if id.nil? - - id = id.id if ActiveRecord::Model === id + def exists?(conditions = :none) + conditions = conditions.id if ActiveRecord::Model === conditions + return false if !conditions join_dependency = construct_join_dependency_for_association_find relation = construct_relation_for_association_find(join_dependency) - relation = relation.except(:select, :order).select("1").limit(1) + relation = relation.except(:select, :order).select("1 AS one").limit(1) - case id + case conditions when Array, Hash - relation = relation.where(id) + relation = relation.where(conditions) else - relation = relation.where(table[primary_key].eq(id)) if id + relation = relation.where(table[primary_key].eq(conditions)) if conditions != :none end - connection.select_value(relation, "#{name} Exists") ? true : false + connection.select_value(relation, "#{name} Exists", relation.bind_values) + rescue ThrowResult + false end protected @@ -215,12 +190,12 @@ module ActiveRecord end def construct_join_dependency_for_association_find - including = (@eager_load_values + @includes_values).uniq + including = (eager_load_values + includes_values).uniq ActiveRecord::Associations::JoinDependency.new(@klass, including, []) end def construct_relation_for_association_calculations - including = (@eager_load_values + @includes_values).uniq + including = (eager_load_values + includes_values).uniq join_dependency = ActiveRecord::Associations::JoinDependency.new(@klass, including, arel.froms.first) relation = except(:includes, :eager_load, :preload) apply_join_dependency(relation, join_dependency) @@ -258,44 +233,6 @@ module ActiveRecord ids_array.empty? ? raise(ThrowResult) : table[primary_key].in(ids_array) end - def find_by_attributes(match, attributes, *args) - conditions = Hash[attributes.map {|a| [a, args[attributes.index(a)]]}] - result = where(conditions).send(match.finder) - - if match.bang? && result.blank? - raise RecordNotFound, "Couldn't find #{@klass.name} with #{conditions.to_a.collect {|p| p.join(' = ')}.join(', ')}" - else - yield(result) if block_given? - result - end - end - - def find_or_instantiator_by_attributes(match, attributes, *args) - options = args.size > 1 && args.last(2).all?{ |a| a.is_a?(Hash) } ? args.extract_options! : {} - protected_attributes_for_create, unprotected_attributes_for_create = {}, {} - args.each_with_index do |arg, i| - if arg.is_a?(Hash) - protected_attributes_for_create = args[i].with_indifferent_access - else - unprotected_attributes_for_create[attributes[i]] = args[i] - end - end - - conditions = (protected_attributes_for_create.merge(unprotected_attributes_for_create)).slice(*attributes).symbolize_keys - - record = where(conditions).first - - unless record - record = @klass.new(protected_attributes_for_create, options) do |r| - r.assign_attributes(unprotected_attributes_for_create, :without_protection => true) - end - yield(record) if block_given? - record.save if match.instantiator == :create - end - - record - end - def find_with_ids(*ids) return to_a.find { |*block_args| yield(*block_args) } if block_given? @@ -318,21 +255,11 @@ module ActiveRecord def find_one(id) id = id.id if ActiveRecord::Base === id - if IdentityMap.enabled? && where_values.blank? && - limit_value.blank? && order_values.blank? && - includes_values.blank? && preload_values.blank? && - readonly_value.nil? && joins_values.blank? && - !@klass.locking_enabled? && - record = IdentityMap.get(@klass, id) - return record - end - column = columns_hash[primary_key] - - substitute = connection.substitute_at(column, @bind_values.length) + substitute = connection.substitute_at(column, bind_values.length) relation = where(table[primary_key].eq(substitute)) - relation.bind_values = [[column, id]] - record = relation.first + relation.bind_values += [[column, id]] + record = relation.take unless record conditions = arel.where_sql @@ -344,18 +271,18 @@ module ActiveRecord end def find_some(ids) - result = where(table[primary_key].in(ids)).all + result = where(table[primary_key].in(ids)).to_a expected_size = - if @limit_value && ids.size > @limit_value - @limit_value + if limit_value && ids.size > limit_value + limit_value else ids.size end # 11 ids with limit 3, offset 9 should give 2 results. - if @offset_value && (ids.size - @offset_value < expected_size) - expected_size = ids.size - @offset_value + if offset_value && (ids.size - offset_value < expected_size) + expected_size = ids.size - offset_value end if result.size == expected_size @@ -370,11 +297,24 @@ module ActiveRecord end end + def find_take + if loaded? + @records.first + else + @take ||= limit(1).to_a.first + end + end + def find_first if loaded? @records.first else - @first ||= limit(1).to_a[0] + @first ||= + if with_default_scope.order_values.empty? && primary_key + order(arel_table[primary_key].asc).limit(1).to_a.first + else + limit(1).to_a.first + end end end @@ -386,7 +326,7 @@ module ActiveRecord if offset_value || limit_value to_a.last else - reverse_order.limit(1).to_a[0] + reverse_order.limit(1).to_a.first end end end diff --git a/activerecord/lib/active_record/relation/merger.rb b/activerecord/lib/active_record/relation/merger.rb new file mode 100644 index 0000000000..e5b50673da --- /dev/null +++ b/activerecord/lib/active_record/relation/merger.rb @@ -0,0 +1,119 @@ +require 'active_support/core_ext/hash/keys' + +module ActiveRecord + class Relation + class HashMerger # :nodoc: + attr_reader :relation, :hash + + def initialize(relation, hash) + hash.assert_valid_keys(*Relation::VALUE_METHODS) + + @relation = relation + @hash = hash + end + + def merge + Merger.new(relation, other).merge + end + + # Applying values to a relation has some side effects. E.g. + # interpolation might take place for where values. So we should + # build a relation to merge in rather than directly merging + # the values. + def other + other = Relation.new(relation.klass, relation.table) + hash.each { |k, v| other.send("#{k}!", v) } + other + end + end + + class Merger # :nodoc: + attr_reader :relation, :values + + def initialize(relation, other) + if other.default_scoped? && other.klass != relation.klass + other = other.with_default_scope + end + + @relation = relation + @values = other.values + end + + def normal_values + Relation::SINGLE_VALUE_METHODS + + Relation::MULTI_VALUE_METHODS - + [:where, :order, :bind, :reverse_order, :lock, :create_with, :reordering, :from] + end + + def merge + normal_values.each do |name| + value = values[name] + relation.send("#{name}!", value) unless value.blank? + end + + merge_multi_values + merge_single_values + + relation + end + + private + + def merge_multi_values + relation.where_values = merged_wheres + relation.bind_values = merged_binds + + if values[:reordering] + # override any order specified in the original relation + relation.reorder! values[:order] + elsif values[:order] + # merge in order_values from r + relation.order! values[:order] + end + + relation.extend(*values[:extending]) unless values[:extending].blank? + end + + def merge_single_values + relation.from_value = values[:from] unless relation.from_value + relation.lock_value = values[:lock] unless relation.lock_value + relation.reverse_order_value = values[:reverse_order] + + unless values[:create_with].blank? + relation.create_with_value = (relation.create_with_value || {}).merge(values[:create_with]) + end + end + + def merged_binds + if values[:bind] + (relation.bind_values + values[:bind]).uniq(&:first) + else + relation.bind_values + end + end + + def merged_wheres + if values[:where] + merged_wheres = relation.where_values + values[:where] + + unless relation.where_values.empty? + # Remove equalities with duplicated left-hand. Last one wins. + seen = {} + merged_wheres = merged_wheres.reverse.reject { |w| + nuke = false + if w.respond_to?(:operator) && w.operator == :== + nuke = seen[w.left] + seen[w.left] = true + end + nuke + }.reverse + end + + merged_wheres + else + relation.where_values + end + end + end + end +end diff --git a/activerecord/lib/active_record/relation/predicate_builder.rb b/activerecord/lib/active_record/relation/predicate_builder.rb index 1d04e763f6..71030cb5d7 100644 --- a/activerecord/lib/active_record/relation/predicate_builder.rb +++ b/activerecord/lib/active_record/relation/predicate_builder.rb @@ -1,66 +1,97 @@ module ActiveRecord class PredicateBuilder # :nodoc: - def self.build_from_hash(engine, attributes, default_table) - predicates = attributes.map do |column, value| + def self.build_from_hash(klass, attributes, default_table) + queries = [] + + attributes.each do |column, value| table = default_table if value.is_a?(Hash) - table = Arel::Table.new(column, engine) - build_from_hash(engine, value, table) + table = Arel::Table.new(column, default_table.engine) + association = klass.reflect_on_association(column.to_sym) + + if value.empty? + queries.concat ['1 = 2'] + else + value.each do |k, v| + queries.concat expand(association && association.klass, table, k, v) + end + end else column = column.to_s if column.include?('.') table_name, column = column.split('.', 2) - table = Arel::Table.new(table_name, engine) + table = Arel::Table.new(table_name, default_table.engine) end - build(table[column.to_sym], value) + queries.concat expand(klass, table, column, value) + end + end + + queries + end + + def self.expand(klass, table, column, value) + queries = [] + + # Find the foreign key when using queries such as: + # Post.where(:author => author) + # + # For polymorphic relationships, find the foreign key and type: + # PriceEstimate.where(:estimate_of => treasure) + if klass && value.class < ActiveRecord::Tag && reflection = klass.reflect_on_association(column.to_sym) + if reflection.polymorphic? + queries << build(table[reflection.foreign_type], value.class.base_class) end + + column = reflection.foreign_key end - predicates.flatten + + queries << build(table[column.to_sym], value) + queries end def self.references(attributes) - references = attributes.map do |key, value| + attributes.map do |key, value| if value.is_a?(Hash) key else key = key.to_s key.split('.').first.to_sym if key.include?('.') end - end - references.compact + end.compact end private def self.build(attribute, value) case value - when ActiveRecord::Relation - value = value.select(value.klass.arel_table[value.klass.primary_key]) 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::Model) ? x.id : x} - ranges, values = values.partition {|v| v.is_a?(Range) || v.is_a?(Arel::Relation)} + ranges, values = values.partition {|v| v.is_a?(Range)} - array_predicates = ranges.map {|range| attribute.in(range)} - - if values.include?(nil) + values_predicate = if values.include?(nil) values = values.compact + case values.length when 0 - array_predicates << attribute.eq(nil) + attribute.eq(nil) when 1 - array_predicates << attribute.eq(values.first).or(attribute.eq(nil)) + attribute.eq(values.first).or(attribute.eq(nil)) else - array_predicates << attribute.in(values).or(attribute.eq(nil)) + attribute.in(values).or(attribute.eq(nil)) end else - array_predicates << attribute.in(values) + attribute.in(values) end - array_predicates.inject {|composite, predicate| composite.or(predicate)} - when Range, Arel::Relation + array_predicates = ranges.map { |range| attribute.in(range) } + array_predicates << values_predicate + array_predicates.inject { |composite, predicate| composite.or(predicate) } + when ActiveRecord::Relation + value = value.select(value.klass.arel_table[value.klass.primary_key]) if value.select_values.empty? + attribute.in(value.arel.ast) + when Range attribute.in(value) when ActiveRecord::Model attribute.eq(value.id) diff --git a/activerecord/lib/active_record/relation/query_methods.rb b/activerecord/lib/active_record/relation/query_methods.rb index 87dd513880..3c59bd8a68 100644 --- a/activerecord/lib/active_record/relation/query_methods.rb +++ b/activerecord/lib/active_record/relation/query_methods.rb @@ -1,66 +1,134 @@ require 'active_support/core_ext/array/wrap' -require 'active_support/core_ext/object/blank' module ActiveRecord module QueryMethods extend ActiveSupport::Concern - attr_accessor :includes_values, :eager_load_values, :preload_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, :reordering_value, :reverse_order_value, - :uniq_value, :references_values + Relation::MULTI_VALUE_METHODS.each do |name| + class_eval <<-CODE, __FILE__, __LINE__ + 1 + def #{name}_values # def select_values + @values[:#{name}] || [] # @values[:select] || [] + end # end + # + def #{name}_values=(values) # def select_values=(values) + raise ImmutableRelation if @loaded # raise ImmutableRelation if @loaded + @values[:#{name}] = values # @values[:select] = values + end # end + CODE + end + + (Relation::SINGLE_VALUE_METHODS - [:create_with]).each do |name| + class_eval <<-CODE, __FILE__, __LINE__ + 1 + def #{name}_value # def readonly_value + @values[:#{name}] # @values[:readonly] + end # end + CODE + end + + Relation::SINGLE_VALUE_METHODS.each do |name| + class_eval <<-CODE, __FILE__, __LINE__ + 1 + def #{name}_value=(value) # def readonly_value=(value) + raise ImmutableRelation if @loaded # raise ImmutableRelation if @loaded + @values[:#{name}] = value # @values[:readonly] = value + end # end + CODE + end + def create_with_value # :nodoc: + @values[:create_with] || {} + end + + alias extensions extending_values + + # Specify relationships to be included in the result set. For + # example: + # + # users = User.includes(:address) + # users.each do |user| + # user.address.city + # end + # + # allows you to access the +address+ attribute of the +User+ model without + # firing an additional query. This will often result in a + # performance improvement over a simple +join+. + # + # === conditions + # + # If you want to add conditions to your included models you'll have + # to explicitly reference them. For example: + # + # User.includes(:posts).where('posts.name = ?', 'example') + # + # Will throw an error, but this will work: + # + # User.includes(:posts).where('posts.name = ?', 'example').references(:posts) def includes(*args) - args.reject! {|a| a.blank? } + args.empty? ? self : spawn.includes!(*args) + end - return self if args.empty? + # Like #includes, but modifies the relation in place. + def includes!(*args) + args.reject! {|a| a.blank? } - relation = clone - relation.includes_values = (relation.includes_values + args).flatten.uniq - relation + self.includes_values = (includes_values + args).flatten.uniq + self end + # Forces eager loading by performing a LEFT OUTER JOIN on +args+: + # + # User.eager_load(:posts) + # => SELECT "users"."id" AS t0_r0, "users"."name" AS t0_r1, ... + # FROM "users" LEFT OUTER JOIN "posts" ON "posts"."user_id" = + # "users"."id" def eager_load(*args) - return self if args.blank? + args.blank? ? self : spawn.eager_load!(*args) + end - relation = clone - relation.eager_load_values += args - relation + # Like #eager_load, but modifies relation in place. + def eager_load!(*args) + self.eager_load_values += args + self end + # Allows preloading of +args+, in the same way that +includes+ does: + # + # User.preload(:posts) + # => SELECT "posts".* FROM "posts" WHERE "posts"."user_id" IN (1, 2, 3) def preload(*args) - return self if args.blank? + args.blank? ? self : spawn.preload!(*args) + end - relation = clone - relation.preload_values += args - relation + # Like #preload, but modifies relation in place. + def preload!(*args) + self.preload_values += args + self end # Used to indicate that an association is referenced by an SQL string, and should # therefore be JOINed in any query rather than loaded separately. # - # For example: - # # User.includes(:posts).where("posts.name = 'foo'") # # => Doesn't JOIN the posts table, resulting in an error. # # User.includes(:posts).where("posts.name = 'foo'").references(:posts) # # => Query now knows the string references posts, so adds a JOIN def references(*args) - return self if args.blank? + args.blank? ? self : spawn.references!(*args) + end - relation = clone - relation.references_values = (references_values + args.flatten.map(&:to_s)).uniq - relation + # Like #references, but modifies relation in place. + def references!(*args) + args.flatten! + + self.references_values = (references_values + args.map!(&:to_s)).uniq + self end # Works in two unique ways. # # First: takes a block so it can be used just like Array#select. # - # Model.scoped.select { |m| m.field == value } + # Model.all.select { |m| m.field == value } # # This will build an array of objects from the database for the scope, # converting them into an array and iterating through them using Array#select. @@ -68,8 +136,8 @@ module ActiveRecord # Second: Modifies the SELECT statement for the query so that only certain # fields are retrieved: # - # >> Model.select(:field) - # => [#<Model field:value>] + # Model.select(:field) + # # => [#<Model field:value>] # # Although in the above example it looks as though this method returns an # array, it actually returns a relation object and can have other query @@ -77,44 +145,77 @@ module ActiveRecord # # The argument to the method can also be an array of fields. # - # >> Model.select([:field, :other_field, :and_one_more]) - # => [#<Model field: "value", other_field: "value", and_one_more: "value">] + # Model.select(:field, :other_field, :and_one_more) + # # => [#<Model field: "value", other_field: "value", and_one_more: "value">] # # Accessing attributes of an object that do not have fields retrieved by a select # will throw <tt>ActiveModel::MissingAttributeError</tt>: # - # >> Model.select(:field).first.other_field - # => ActiveModel::MissingAttributeError: missing attribute: other_field - def select(value = Proc.new) + # Model.select(:field).first.other_field + # # => ActiveModel::MissingAttributeError: missing attribute: other_field + def select(*fields) if block_given? - to_a.select {|*block_args| value.call(*block_args) } + to_a.select { |*block_args| yield(*block_args) } else - relation = clone - relation.select_values += Array.wrap(value) - relation + raise ArgumentError, 'Call this with at least one field' if fields.empty? + spawn.select!(*fields) end end + # Like #select, but modifies relation in place. + def select!(*fields) + self.select_values += fields.flatten + self + end + + # Allows to specify a group attribute: + # + # User.group(:name) + # => SELECT "users".* FROM "users" GROUP BY name + # + # Returns an array with distinct records based on the +group+ attribute: + # + # User.select([:id, :name]) + # => [#<User id: 1, name: "Oscar">, #<User id: 2, name: "Oscar">, #<User id: 3, name: "Foo"> + # + # User.group(:name) + # => [#<User id: 3, name: "Foo", ...>, #<User id: 2, name: "Oscar", ...>] def group(*args) - return self if args.blank? + args.blank? ? self : spawn.group!(*args) + end + + # Like #group, but modifies relation in place. + def group!(*args) + args.flatten! - relation = clone - relation.group_values += args.flatten - relation + self.group_values += args + self end + # Allows to specify an order attribute: + # + # User.order('name') + # => SELECT "users".* FROM "users" ORDER BY name + # + # User.order('name DESC') + # => SELECT "users".* FROM "users" ORDER BY name DESC + # + # User.order('name DESC, email') + # => SELECT "users".* FROM "users" ORDER BY name DESC, email def order(*args) - return self if args.blank? + args.blank? ? self : spawn.order!(*args) + end + + # Like #order, but modifies relation in place. + def order!(*args) + args.flatten! - args = args.flatten references = args.reject { |arg| Arel::Node === arg } - .map { |arg| arg =~ /^([a-zA-Z]\w*)\.(\w+)/ && $1 } - .compact + references.map! { |arg| arg =~ /^([a-zA-Z]\w*)\.(\w+)/ && $1 }.compact! + references!(references) if references.any? - relation = clone - relation = relation.references(references) if references.any? - relation.order_values += args - relation + self.order_values = args + self.order_values + self end # Replaces any existing order defined on the relation with the specified order. @@ -125,83 +226,238 @@ module ActiveRecord # # User.order('email DESC').reorder('id ASC').order('name ASC') # - # generates a query with 'ORDER BY id ASC, name ASC'. - # + # generates a query with 'ORDER BY name ASC, id ASC'. def reorder(*args) - return self if args.blank? + args.blank? ? self : spawn.reorder!(*args) + end + + # Like #reorder, but modifies relation in place. + def reorder!(*args) + args.flatten! - relation = clone - relation.reordering_value = true - relation.order_values = args.flatten - relation + self.reordering_value = true + self.order_values = args + self end + # Performs a joins on +args+: + # + # User.joins(:posts) + # => SELECT "users".* FROM "users" INNER JOIN "posts" ON "posts"."user_id" = "users"."id" def joins(*args) - return self if args.compact.blank? - - relation = clone + args.compact.blank? ? self : spawn.joins!(*args) + end + # Like #joins, but modifies relation in place. + def joins!(*args) args.flatten! - relation.joins_values += args - relation + self.joins_values += args + self end def bind(value) - relation = clone - relation.bind_values += [value] - relation + spawn.bind!(value) + end + + def bind!(value) + self.bind_values += [value] + self end + # Returns a new relation, which is the result of filtering the current relation + # according to the conditions in the arguments. + # + # #where accepts conditions in one of several formats. In the examples below, the resulting + # SQL is given as an illustration; the actual query generated may be different depending + # on the database adapter. + # + # === string + # + # A single string, without additional arguments, is passed to the query + # constructor as a SQL fragment, and used in the where clause of the query. + # + # Client.where("orders_count = '2'") + # # SELECT * from clients where orders_count = '2'; + # + # Note that building your own string from user input may expose your application + # to injection attacks if not done properly. As an alternative, it is recommended + # to use one of the following methods. + # + # === array + # + # If an array is passed, then the first element of the array is treated as a template, and + # the remaining elements are inserted into the template to generate the condition. + # Active Record takes care of building the query to avoid injection attacks, and will + # convert from the ruby type to the database type where needed. Elements are inserted + # into the string in the order in which they appear. + # + # User.where(["name = ? and email = ?", "Joe", "joe@example.com"]) + # # SELECT * FROM users WHERE name = 'Joe' AND email = 'joe@example.com'; + # + # Alternatively, you can use named placeholders in the template, and pass a hash as the + # second element of the array. The names in the template are replaced with the corresponding + # values from the hash. + # + # User.where(["name = :name and email = :email", { name: "Joe", email: "joe@example.com" }]) + # # SELECT * FROM users WHERE name = 'Joe' AND email = 'joe@example.com'; + # + # This can make for more readable code in complex queries. + # + # Lastly, you can use sprintf-style % escapes in the template. This works slightly differently + # than the previous methods; you are responsible for ensuring that the values in the template + # are properly quoted. The values are passed to the connector for quoting, but the caller + # is responsible for ensuring they are enclosed in quotes in the resulting SQL. After quoting, + # the values are inserted using the same escapes as the Ruby core method <tt>Kernel::sprintf</tt>. + # + # User.where(["name = '%s' and email = '%s'", "Joe", "joe@example.com"]) + # # SELECT * FROM users WHERE name = 'Joe' AND email = 'joe@example.com'; + # + # If #where is called with multiple arguments, these are treated as if they were passed as + # the elements of a single array. + # + # User.where("name = :name and email = :email", { name: "Joe", email: "joe@example.com" }) + # # SELECT * FROM users WHERE name = 'Joe' AND email = 'joe@example.com'; + # + # When using strings to specify conditions, you can use any operator available from + # the database. While this provides the most flexibility, you can also unintentionally introduce + # dependencies on the underlying database. If your code is intended for general consumption, + # test with multiple database backends. + # + # === hash + # + # #where will also accept a hash condition, in which the keys are fields and the values + # are values to be searched for. + # + # Fields can be symbols or strings. Values can be single values, arrays, or ranges. + # + # User.where({ name: "Joe", email: "joe@example.com" }) + # # SELECT * FROM users WHERE name = 'Joe' AND email = 'joe@example.com' + # + # User.where({ name: ["Alice", "Bob"]}) + # # SELECT * FROM users WHERE name IN ('Alice', 'Bob') + # + # User.where({ created_at: (Time.now.midnight - 1.day)..Time.now.midnight }) + # # SELECT * FROM users WHERE (created_at BETWEEN '2012-06-09 07:00:00.000000' AND '2012-06-10 07:00:00.000000') + # + # In the case of a belongs_to relationship, an association key can be used + # to specify the model if an ActiveRecord object is used as the value. + # + # author = Author.find(1) + # + # # The following queries will be equivalent: + # Post.where(:author => author) + # Post.where(:author_id => author) + # + # This also works with polymorphic belongs_to relationships: + # + # treasure = Treasure.create(:name => 'gold coins') + # treasure.price_estimates << PriceEstimate.create(:price => 125) + # + # # The following queries will be equivalent: + # PriceEstimate.where(:estimate_of => treasure) + # PriceEstimate.where(:estimate_of_type => 'Treasure', :estimate_of_id => treasure) + # + # === Joins + # + # If the relation is the result of a join, you may create a condition which uses any of the + # tables in the join. For string and array conditions, use the table name in the condition. + # + # User.joins(:posts).where("posts.created_at < ?", Time.now) + # + # For hash conditions, you can either use the table name in the key, or use a sub-hash. + # + # User.joins(:posts).where({ "posts.published" => true }) + # User.joins(:posts).where({ :posts => { :published => true } }) + # + # === empty condition + # + # If the condition returns true for blank?, then where is a no-op and returns the current relation. def where(opts, *rest) - return self if opts.blank? + opts.blank? ? self : spawn.where!(opts, *rest) + end - relation = clone - relation = relation.references(PredicateBuilder.references(opts)) if Hash === opts - relation.where_values += build_where(opts, rest) - relation + # #where! is identical to #where, except that instead of returning a new relation, it adds + # the condition to the existing relation. + def where!(opts, *rest) + references!(PredicateBuilder.references(opts)) if Hash === opts + + self.where_values += build_where(opts, rest) + self end + # Allows to specify a HAVING clause. Note that you can't use HAVING + # without also specifying a GROUP clause. + # + # Order.having('SUM(price) > 30').group('user_id') def having(opts, *rest) - return self if opts.blank? + opts.blank? ? self : spawn.having!(opts, *rest) + end + + # Like #having, but modifies relation in place. + def having!(opts, *rest) + references!(PredicateBuilder.references(opts)) if Hash === opts - relation = clone - relation = relation.references(PredicateBuilder.references(opts)) if Hash === opts - relation.having_values += build_where(opts, rest) - relation + self.having_values += build_where(opts, rest) + self end + # Specifies a limit for the number of records to retrieve. + # + # User.limit(10) # generated SQL has 'LIMIT 10' + # + # User.limit(10).limit(20) # generated SQL has 'LIMIT 20' def limit(value) - relation = clone - relation.limit_value = value - relation + spawn.limit!(value) end + # Like #limit, but modifies relation in place. + def limit!(value) + self.limit_value = value + self + end + + # Specifies the number of rows to skip before returning rows. + # + # User.offset(10) # generated SQL has "OFFSET 10" + # + # Should be used with order. + # + # User.offset(10).order("name ASC") def offset(value) - relation = clone - relation.offset_value = value - relation + spawn.offset!(value) + end + + # Like #offset, but modifies relation in place. + def offset!(value) + self.offset_value = value + self end + # Specifies locking settings (default to +true+). For more information + # on locking, please see +ActiveRecord::Locking+. def lock(locks = true) - relation = clone + spawn.lock!(locks) + end + # Like #lock, but modifies relation in place. + def lock!(locks = true) case locks when String, TrueClass, NilClass - relation.lock_value = locks || true + self.lock_value = locks || true else - relation.lock_value = false + self.lock_value = false end - relation + self end # Returns a chainable relation with zero records, specifically an - # instance of the NullRelation class. + # instance of the <tt>ActiveRecord::NullRelation</tt> class. # - # The returned NullRelation inherits from Relation and implements the - # Null Object pattern so it is an object with defined null behavior: - # it always returns an empty array of records and does not query the database. + # The returned <tt>ActiveRecord::NullRelation</tt> inherits from Relation and implements the + # Null Object pattern. It is an object with defined null behavior and always returns an empty + # array of records without quering the database. # # Any subsequent condition chained to the returned relation will continue # generating an empty relation and will not fire any query to the database. @@ -226,25 +482,73 @@ module ActiveRecord # end # def none - NullRelation.new(@klass, @table) + extending(NullRelation) end + # Sets readonly attributes for the returned relation. If value is + # true (default), attempting to update a record will result in an error. + # + # users = User.readonly + # users.first.save + # => ActiveRecord::ReadOnlyRecord: ActiveRecord::ReadOnlyRecord def readonly(value = true) - relation = clone - relation.readonly_value = value - relation + spawn.readonly!(value) + end + + # Like #readonly, but modifies relation in place. + def readonly!(value = true) + self.readonly_value = value + self end + # Sets attributes to be used when creating new records from a + # relation object. + # + # users = User.where(name: 'Oscar') + # users.new.name # => 'Oscar' + # + # users = users.create_with(name: 'DHH') + # users.new.name # => 'DHH' + # + # You can pass +nil+ to +create_with+ to reset attributes: + # + # users = users.create_with(nil) + # users.new.name # => 'Oscar' def create_with(value) - relation = clone - relation.create_with_value = value ? create_with_value.merge(value) : {} - relation + spawn.create_with!(value) + end + + # Like #create_with but modifies the relation in place. Raises + # +ImmutableRelation+ if the relation has already been loaded. + # + # users = User.all.create_with!(name: 'Oscar') + # users.new.name # => 'Oscar' + def create_with!(value) + self.create_with_value = value ? create_with_value.merge(value) : {} + self + end + + # Specifies table from which the records will be fetched. For example: + # + # Topic.select('title').from('posts') + # #=> SELECT title FROM posts + # + # Can accept other relation objects. For example: + # + # Topic.select('title').from(Topic.approved) + # # => SELECT title FROM (SELECT * FROM topics WHERE approved = 't') subquery + # + # Topic.select('a.title').from(Topic.approved, :a) + # # => SELECT a.title FROM (SELECT * FROM topics WHERE approved = 't') a + # + def from(value, subquery_name = nil) + spawn.from!(value, subquery_name) end - def from(value) - relation = clone - relation.from_value = value - relation + # Like #from, but modifies relation in place. + def from!(value, subquery_name = nil) + self.from_value = [value, subquery_name] + self end # Specifies whether the records should be unique or not. For example: @@ -258,9 +562,13 @@ module ActiveRecord # User.select(:name).uniq.uniq(false) # # => You can also remove the uniqueness def uniq(value = true) - relation = clone - relation.uniq_value = value - relation + spawn.uniq!(value) + end + + # Like #uniq, but modifies relation in place. + def uniq!(value = true) + self.uniq_value = value + self end # Used to extend a scope with additional methods, either through @@ -276,16 +584,16 @@ module ActiveRecord # end # end # - # scope = Model.scoped.extending(Pagination) + # scope = Model.all.extending(Pagination) # scope.page(params[:page]) # # You can also pass a list of modules: # - # scope = Model.scoped.extending(Pagination, SomethingElse) + # scope = Model.all.extending(Pagination, SomethingElse) # # === Using a block # - # scope = Model.scoped.extending do + # scope = Model.all.extending do # def page(number) # # pagination code goes here # end @@ -294,54 +602,71 @@ module ActiveRecord # # You can also use a block and a module list: # - # scope = Model.scoped.extending(Pagination) do + # scope = Model.all.extending(Pagination) do # def per_page(number) # # pagination code goes here # end # end - def extending(*modules) - modules << Module.new(&Proc.new) if block_given? + def extending(*modules, &block) + if modules.any? || block + spawn.extending!(*modules, &block) + else + self + end + end - return self if modules.empty? + # Like #extending, but modifies relation in place. + def extending!(*modules, &block) + modules << Module.new(&block) if block_given? - relation = clone - relation.send(:apply_modules, modules.flatten) - relation + self.extending_values += modules.flatten + extend(*extending_values) if extending_values.any? + + self end + # Reverse the existing order clause on the relation. + # + # User.order('name ASC').reverse_order # generated SQL has 'ORDER BY name DESC' def reverse_order - relation = clone - relation.reverse_order_value = !relation.reverse_order_value - relation + spawn.reverse_order! + end + + # Like #reverse_order, but modifies relation in place. + def reverse_order! + self.reverse_order_value = !reverse_order_value + self end + # Returns the Arel object associated with the relation. def arel @arel ||= with_default_scope.build_arel end + # Like #arel, but ignores the default scope of the model. def build_arel - arel = table.from table + arel = Arel::SelectManager.new(table.engine, table) - build_joins(arel, @joins_values) unless @joins_values.empty? + build_joins(arel, joins_values) unless joins_values.empty? - collapse_wheres(arel, (@where_values - ['']).uniq) + collapse_wheres(arel, (where_values - ['']).uniq) - arel.having(*@having_values.uniq.reject{|h| h.blank?}) unless @having_values.empty? + arel.having(*having_values.uniq.reject{|h| h.blank?}) unless having_values.empty? - arel.take(connection.sanitize_limit(@limit_value)) if @limit_value - arel.skip(@offset_value) if @offset_value + arel.take(connection.sanitize_limit(limit_value)) if limit_value + arel.skip(offset_value.to_i) if offset_value - arel.group(*@group_values.uniq.reject{|g| g.blank?}) unless @group_values.empty? + arel.group(*group_values.uniq.reject{|g| g.blank?}) unless group_values.empty? - order = @order_values - order = reverse_sql_order(order) if @reverse_order_value + order = order_values + order = reverse_sql_order(order) if reverse_order_value arel.order(*order.uniq.reject{|o| o.blank?}) unless order.empty? - build_select(arel, @select_values.uniq) + build_select(arel, select_values.uniq) - arel.distinct(@uniq_value) - arel.from(@from_value) if @from_value - arel.lock(@lock_value) if @lock_value + arel.distinct(uniq_value) + arel.from(build_from) if from_value + arel.lock(lock_value) if lock_value arel end @@ -383,12 +708,23 @@ module ActiveRecord [@klass.send(:sanitize_sql, other.empty? ? opts : ([opts] + other))] when Hash attributes = @klass.send(:expand_hash_conditions_for_aggregates, opts) - PredicateBuilder.build_from_hash(table.engine, attributes, table) + PredicateBuilder.build_from_hash(klass, attributes, table) else [opts] end end + def build_from + opts, name = from_value + case opts + when Relation + name ||= 'subquery' + opts.arel.as(name.to_s) + else + opts + end + end + def build_joins(manager, joins) buckets = joins.group_by do |join| case join @@ -443,13 +779,6 @@ module ActiveRecord end end - def apply_modules(modules) - unless modules.empty? - @extensions += modules - modules.each {|extension| extend(extension) } - end - end - def reverse_sql_order(order_query) order_query = ["#{quoted_table_name}.#{quoted_primary_key} ASC"] if order_query.empty? diff --git a/activerecord/lib/active_record/relation/spawn_methods.rb b/activerecord/lib/active_record/relation/spawn_methods.rb index 7131aa29b6..5394c1b28b 100644 --- a/activerecord/lib/active_record/relation/spawn_methods.rb +++ b/activerecord/lib/active_record/relation/spawn_methods.rb @@ -1,75 +1,53 @@ -require 'active_support/core_ext/object/blank' +require 'active_support/core_ext/hash/except' +require 'active_support/core_ext/hash/slice' +require 'active_record/relation/merger' module ActiveRecord module SpawnMethods - def merge(r) - return self unless r - return to_a & r if r.is_a?(Array) - merged_relation = clone - - r = r.with_default_scope if r.default_scoped? && r.klass != klass - - Relation::ASSOCIATION_METHODS.each do |method| - value = r.send(:"#{method}_values") - - unless value.empty? - if method == :includes - merged_relation = merged_relation.includes(value) - else - merged_relation.send(:"#{method}_values=", value) - end - end - end - - (Relation::MULTI_VALUE_METHODS - [:joins, :where, :order]).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 + # This is overridden by Associations::CollectionProxy + def spawn #:nodoc: + clone + end - (Relation::SINGLE_VALUE_METHODS - [:lock, :create_with, :reordering]).each do |method| - value = r.send(:"#{method}_value") - merged_relation.send(:"#{method}_value=", value) unless value.nil? + # Merges in the conditions from <tt>other</tt>, if <tt>other</tt> is an <tt>ActiveRecord::Relation</tt>. + # Returns an array representing the intersection of the resulting records with <tt>other</tt>, if <tt>other</tt> is an array. + # + # ==== Examples + # + # Post.where(:published => true).joins(:comments).merge( Comment.where(:spam => false) ) + # # Performs a single join query with both where conditions. + # + # recent_posts = Post.order('created_at DESC').first(5) + # Post.where(:published => true).merge(recent_posts) + # # Returns the intersection of all published posts with the 5 most recently created posts. + # # (This is just an example. You'd probably want to do this with a single query!) + # + # Procs will be evaluated by merge: + # + # Post.where(published: true).merge(-> { joins(:comments) }) + # # => Post.where(published: true).joins(:comments) + # + # This is mainly intended for sharing common conditions between multiple associations. + # + def merge(other) + if other.is_a?(Array) + to_a & other + elsif other + spawn.merge!(other) + else + self end + end - merged_relation.lock_value = r.lock_value unless merged_relation.lock_value - - merged_relation = merged_relation.create_with(r.create_with_value) unless r.create_with_value.empty? - - if (r.reordering_value) - # override any order specified in the original relation - merged_relation.reordering_value = true - merged_relation.order_values = r.order_values + # Like #merge, but applies changes in place. + def merge!(other) + if !other.is_a?(Relation) && other.respond_to?(:to_proc) + instance_exec(&other) else - # merge in order_values from r - merged_relation.order_values += r.order_values + klass = other.is_a?(Hash) ? Relation::HashMerger : Relation::Merger + klass.new(self, other).merge end - - # Apply scope extension modules - merged_relation.send :apply_modules, r.extensions - - merged_relation end # Removes from the query the condition(s) specified in +skips+. @@ -80,20 +58,9 @@ module ActiveRecord # 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) + result = Relation.new(klass, table, values.except(*skips)) result.default_scoped = default_scoped - - ((Relation::ASSOCIATION_METHODS + Relation::MULTI_VALUE_METHODS) - skips).each do |method| - result.send(:"#{method}_values=", send(:"#{method}_values")) - end - - (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.extend(*extending_values) if extending_values.any? result end @@ -105,44 +72,11 @@ module ActiveRecord # Post.order('id asc').only(:where, :order) # uses the specified order # def only(*onlies) - result = self.class.new(@klass, table) + result = Relation.new(klass, table, values.slice(*onlies)) result.default_scoped = default_scoped - - ((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.extend(*extending_values) if extending_values.any? result end - VALID_FIND_OPTIONS = [ :conditions, :include, :joins, :limit, :offset, :extend, :references, - :order, :select, :readonly, :group, :having, :from, :lock ] - - def apply_finder_options(options) - relation = clone - return relation unless options - - options.assert_valid_keys(VALID_FIND_OPTIONS) - finders = options.dup - finders.delete_if { |key, value| value.nil? && key != :limit } - - ((VALID_FIND_OPTIONS - [:conditions, :include, :extend]) & finders.keys).each do |finder| - relation = relation.send(finder, finders[finder]) - end - - 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 - end end diff --git a/activerecord/lib/active_record/result.rb b/activerecord/lib/active_record/result.rb index fb4b89b87b..425b9b41d8 100644 --- a/activerecord/lib/active_record/result.rb +++ b/activerecord/lib/active_record/result.rb @@ -10,11 +10,11 @@ module ActiveRecord attr_reader :columns, :rows, :column_types - def initialize(columns, rows) + def initialize(columns, rows, column_types = {}) @columns = columns @rows = rows @hash_rows = nil - @column_types = {} + @column_types = column_types end def each @@ -28,6 +28,7 @@ module ActiveRecord alias :map! :map alias :collect! :map + # Returns true if there are no records. def empty? rows.empty? end @@ -52,9 +53,15 @@ module ActiveRecord private def hash_rows - @hash_rows ||= @rows.map { |row| - Hash[@columns.zip(row)] - } + @hash_rows ||= + begin + # We freeze the strings to prevent them getting duped when + # used as keys in ActiveRecord::Model's @attributes hash + columns = @columns.map { |c| c.dup.freeze } + @rows.map { |row| + Hash[columns.zip(row)] + } + end end end end diff --git a/activerecord/lib/active_record/sanitization.rb b/activerecord/lib/active_record/sanitization.rb index 2d7d83d160..42b4cff4b8 100644 --- a/activerecord/lib/active_record/sanitization.rb +++ b/activerecord/lib/active_record/sanitization.rb @@ -1,5 +1,3 @@ -require 'active_support/concern' - module ActiveRecord module Sanitization extend ActiveSupport::Concern @@ -37,9 +35,9 @@ module ActiveRecord # { :name => nil, :group_id => 4 } returns "name = NULL , group_id='4'" def sanitize_sql_for_assignment(assignments) case assignments - when Array; sanitize_sql_array(assignments) - when Hash; sanitize_sql_hash_for_assignment(assignments) - else assignments + when Array; sanitize_sql_array(assignments) + when Hash; sanitize_sql_hash_for_assignment(assignments) + else assignments end end @@ -57,8 +55,8 @@ module ActiveRecord def expand_hash_conditions_for_aggregates(attrs) expanded_attrs = {} attrs.each do |attr, value| - unless (aggregation = reflect_on_aggregation(attr.to_sym)).nil? - mapping = aggregate_mapping(aggregation) + if aggregation = reflect_on_aggregation(attr.to_sym) + mapping = aggregation.mapping mapping.each do |field_attr, aggregate_attr| if mapping.size == 1 && !value.respond_to?(aggregate_attr) expanded_attrs[field_attr] = value @@ -90,8 +88,8 @@ module ActiveRecord def sanitize_sql_hash_for_conditions(attrs, default_table_name = self.table_name) attrs = expand_hash_conditions_for_aggregates(attrs) - table = Arel::Table.new(table_name).alias(default_table_name) - PredicateBuilder.build_from_hash(arel_engine, attrs, table).map { |b| + table = Arel::Table.new(table_name, arel_engine).alias(default_table_name) + PredicateBuilder.build_from_hash(self.class, attrs, table).map { |b| connection.visitor.accept b }.join(' AND ') end @@ -180,15 +178,8 @@ module ActiveRecord end # TODO: Deprecate this - def quoted_id #:nodoc: - quote_value(id, column_for_attribute(self.class.primary_key)) - end - - private - - # Quote strings appropriately for SQL statements. - def quote_value(value, column = nil) - self.class.connection.quote(value, column) + def quoted_id + self.class.quote_value(id, column_for_attribute(self.class.primary_key)) end end end diff --git a/activerecord/lib/active_record/schema.rb b/activerecord/lib/active_record/schema.rb index d815ab05ac..eaa4aa7086 100644 --- a/activerecord/lib/active_record/schema.rb +++ b/activerecord/lib/active_record/schema.rb @@ -1,4 +1,3 @@ -require 'active_support/core_ext/object/blank' module ActiveRecord # = Active Record Schema @@ -12,16 +11,16 @@ module ActiveRecord # # ActiveRecord::Schema.define do # create_table :authors do |t| - # t.string :name, :null => false + # t.string :name, null: false # end # # add_index :authors, :name, :unique # # create_table :posts do |t| - # t.integer :author_id, :null => false + # t.integer :author_id, null: false # t.string :subject # t.text :body - # t.boolean :private, :default => false + # t.boolean :private, default: false # end # # add_index :posts, :author_id @@ -34,6 +33,15 @@ module ActiveRecord ActiveRecord::Migrator.migrations_paths end + def define(info, &block) + instance_eval(&block) + + unless info[:version].blank? + initialize_schema_migrations_table + assume_migrated_upto_version(info[:version], migrations_paths) + end + end + # Eval the given block. All methods available to the current connection # adapter are available within the block, so you can easily use the # database definition DSL to build up your schema (+create_table+, @@ -42,17 +50,11 @@ module ActiveRecord # The +info+ hash is optional, and if given is used to define metadata # about the current schema (currently, only the schema's version): # - # ActiveRecord::Schema.define(:version => 20380119000001) do + # ActiveRecord::Schema.define(version: 20380119000001) do # ... # end def self.define(info={}, &block) - schema = new - schema.instance_eval(&block) - - unless info[:version].blank? - initialize_schema_migrations_table - assume_migrated_upto_version(info[:version], schema.migrations_paths) - end + new.define(info, &block) end end end diff --git a/activerecord/lib/active_record/schema_dumper.rb b/activerecord/lib/active_record/schema_dumper.rb index dcbd165e58..36bde44e7c 100644 --- a/activerecord/lib/active_record/schema_dumper.rb +++ b/activerecord/lib/active_record/schema_dumper.rb @@ -38,9 +38,9 @@ module ActiveRecord end def header(stream) - define_params = @version ? ":version => #{@version}" : "" + define_params = @version ? "version: #{@version}" : "" - if stream.respond_to?(:external_encoding) + if stream.respond_to?(:external_encoding) && stream.external_encoding stream.puts "# encoding: #{stream.external_encoding.name}" end @@ -55,7 +55,7 @@ module ActiveRecord # from scratch. The latter is a flawed and unsustainable approach (the more migrations # you'll amass, the slower it'll run and the greater likelihood for issues). # -# It's strongly recommended to check this file into your version control system. +# It's strongly recommended that you check this file into your version control system. ActiveRecord::Schema.define(#{define_params}) do @@ -70,8 +70,8 @@ HEADER @connection.tables.sort.each do |tbl| next if ['schema_migrations', ignore_tables].flatten.any? do |ignored| case ignored - when String; tbl == ignored - when Regexp; tbl =~ ignored + when String; remove_prefix_and_suffix(tbl) == ignored + when Regexp; remove_prefix_and_suffix(tbl) =~ ignored else raise StandardError, 'ActiveRecord::SchemaDumper.ignore_tables accepts an array of String and / or Regexp values.' end @@ -92,42 +92,26 @@ HEADER pk = @connection.primary_key(table) end - tbl.print " create_table #{table.inspect}" + tbl.print " create_table #{remove_prefix_and_suffix(table).inspect}" if columns.detect { |c| c.name == pk } if pk != 'id' - tbl.print %Q(, :primary_key => "#{pk}") + tbl.print %Q(, primary_key: "#{pk}") end else - tbl.print ", :id => false" + tbl.print ", id: false" end - tbl.print ", :force => true" + tbl.print ", force: true" tbl.puts " do |t|" # then dump all non-primary key columns column_specs = columns.map do |column| raise StandardError, "Unknown type '#{column.sql_type}' for column '#{column.name}'" if @types[column.type].nil? next if column.name == pk - spec = {} - spec[:name] = column.name.inspect - - # AR has an optimization which handles zero-scale decimals as integers. This - # code ensures that the dumper still dumps the column as a decimal. - spec[:type] = if column.type == :integer && /^(numeric|decimal)/ =~ column.sql_type - 'decimal' - else - 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 - 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 + @connection.column_spec(column, @types) end.compact # find all migration keys used in this table - keys = [:name, :limit, :precision, :scale, :default, :null] + keys = @connection.migration_keys # figure out the lengths for each column based on above keys lengths = keys.map { |key| @@ -170,34 +154,23 @@ HEADER stream end - def default_string(value) - case value - when BigDecimal - value.to_s - when Date, DateTime, Time - "'" + value.to_s(:db) + "'" - else - value.inspect - end - end - def indexes(table, stream) if (indexes = @connection.indexes(table)).any? add_index_statements = indexes.map do |index| statement_parts = [ - ('add_index ' + index.table.inspect), + ('add_index ' + remove_prefix_and_suffix(index.table).inspect), index.columns.inspect, - (':name => ' + index.name.inspect), + ('name: ' + index.name.inspect), ] - statement_parts << ':unique => true' if index.unique + statement_parts << 'unique: true' if index.unique index_lengths = (index.lengths || []).compact - statement_parts << (':length => ' + Hash[index.columns.zip(index.lengths)].inspect) unless index_lengths.empty? + statement_parts << ('length: ' + Hash[index.columns.zip(index.lengths)].inspect) unless index_lengths.empty? index_orders = (index.orders || {}) - statement_parts << (':order => ' + index.orders.inspect) unless index_orders.empty? + statement_parts << ('order: ' + index.orders.inspect) unless index_orders.empty? - statement_parts << (':where => ' + index.where.inspect) if index.where + statement_parts << ('where: ' + index.where.inspect) if index.where ' ' + statement_parts.join(', ') end @@ -206,5 +179,9 @@ HEADER stream.puts end end + + def remove_prefix_and_suffix(table) + table.gsub(/^(#{ActiveRecord::Base.table_name_prefix})(.+)(#{ActiveRecord::Base.table_name_suffix})$/, "\\2") + end end end diff --git a/activerecord/lib/active_record/schema_migration.rb b/activerecord/lib/active_record/schema_migration.rb index 236ec563d2..9830abe7d8 100644 --- a/activerecord/lib/active_record/schema_migration.rb +++ b/activerecord/lib/active_record/schema_migration.rb @@ -4,10 +4,13 @@ require 'active_record/base' module ActiveRecord class SchemaMigration < ActiveRecord::Base - attr_accessible :version def self.table_name - Base.table_name_prefix + 'schema_migrations' + Base.table_name_suffix + "#{Base.table_name_prefix}schema_migrations#{Base.table_name_suffix}" + end + + def self.index_name + "#{Base.table_name_prefix}unique_schema_migrations#{Base.table_name_suffix}" end def self.create_table @@ -15,14 +18,13 @@ module ActiveRecord connection.create_table(table_name, :id => false) do |t| t.column :version, :string, :null => false end - connection.add_index table_name, :version, :unique => true, - :name => "#{Base.table_name_prefix}unique_schema_migrations#{Base.table_name_suffix}" + connection.add_index table_name, :version, :unique => true, :name => index_name end end def self.drop_table if connection.table_exists?(table_name) - connection.remove_index table_name, :name => "#{Base.table_name_prefix}unique_schema_migrations#{Base.table_name_suffix}" + connection.remove_index table_name, :name => index_name connection.drop_table(table_name) end end diff --git a/activerecord/lib/active_record/scoping.rb b/activerecord/lib/active_record/scoping.rb index a8f5e96190..0c3fd1bd29 100644 --- a/activerecord/lib/active_record/scoping.rb +++ b/activerecord/lib/active_record/scoping.rb @@ -1,4 +1,3 @@ -require 'active_support/concern' module ActiveRecord module Scoping @@ -10,118 +9,6 @@ module ActiveRecord end module ClassMethods - # with_scope lets you apply options to inner block incrementally. It takes a hash and the keys must be - # <tt>:find</tt> or <tt>:create</tt>. <tt>:find</tt> parameter is <tt>Relation</tt> while - # <tt>:create</tt> parameters are an attributes hash. - # - # class Article < ActiveRecord::Base - # def self.create_with_scope - # with_scope(:find => where(:blog_id => 1), :create => { :blog_id => 1 }) do - # find(1) # => SELECT * from articles WHERE blog_id = 1 AND id = 1 - # a = create(1) - # a.blog_id # => 1 - # end - # end - # end - # - # In nested scopings, all previous parameters are overwritten by the innermost rule, with the exception of - # <tt>where</tt>, <tt>includes</tt>, and <tt>joins</tt> operations in <tt>Relation</tt>, which are merged. - # - # <tt>joins</tt> operations are uniqued so multiple scopes can join in the same table without table aliasing - # problems. If you need to join multiple tables, but still want one of the tables to be uniqued, use the - # array of strings format for your joins. - # - # class Article < ActiveRecord::Base - # def self.find_with_scope - # with_scope(:find => where(:blog_id => 1).limit(1), :create => { :blog_id => 1 }) do - # with_scope(:find => limit(10)) do - # all # => SELECT * from articles WHERE blog_id = 1 LIMIT 10 - # end - # with_scope(:find => where(:author_id => 3)) do - # all # => SELECT * from articles WHERE blog_id = 1 AND author_id = 3 LIMIT 1 - # end - # end - # end - # end - # - # You can ignore any previous scopings by using the <tt>with_exclusive_scope</tt> method. - # - # class Article < ActiveRecord::Base - # def self.find_with_exclusive_scope - # with_scope(:find => where(:blog_id => 1).limit(1)) do - # with_exclusive_scope(:find => limit(10)) do - # all # => SELECT * from articles LIMIT 10 - # end - # end - # end - # end - # - # *Note*: the +:find+ scope also has effect on update and deletion methods, like +update_all+ and +delete_all+. - def with_scope(scope = {}, action = :merge, &block) - # If another Active Record class has been passed in, get its current scope - scope = scope.current_scope if !scope.is_a?(Relation) && scope.respond_to?(:current_scope) - - previous_scope = self.current_scope - - if scope.is_a?(Hash) - # Dup first and second level of hash (method and params). - scope = scope.dup - scope.each do |method, params| - scope[method] = params.dup unless params == true - end - - scope.assert_valid_keys([ :find, :create ]) - relation = construct_finder_arel(scope[:find] || {}) - relation.default_scoped = true unless action == :overwrite - - if previous_scope && previous_scope.create_with_value && scope[:create] - scope_for_create = if action == :merge - previous_scope.create_with_value.merge(scope[:create]) - else - scope[:create] - end - - relation = relation.create_with(scope_for_create) - else - scope_for_create = scope[:create] - scope_for_create ||= previous_scope.create_with_value if previous_scope - relation = relation.create_with(scope_for_create) if scope_for_create - end - - scope = relation - end - - scope = previous_scope.merge(scope) if previous_scope && action == :merge - - self.current_scope = scope - begin - yield - ensure - self.current_scope = previous_scope - end - end - - protected - - # Works like with_scope, but discards any nested properties. - def with_exclusive_scope(method_scoping = {}, &block) - if method_scoping.values.any? { |e| e.is_a?(ActiveRecord::Relation) } - raise ArgumentError, <<-MSG - New finder API can not be used with_exclusive_scope. You can either call unscoped to get an anonymous scope not bound to the default_scope: - - User.unscoped.where(:active => true) - - Or call unscoped with a block: - - User.unscoped do - User.where(:active => true).all - end - - MSG - end - with_scope(method_scoping, :overwrite, &block) - end - def current_scope #:nodoc: Thread.current["#{self}_current_scope"] end @@ -129,15 +16,6 @@ module ActiveRecord def current_scope=(scope) #:nodoc: Thread.current["#{self}_current_scope"] = scope end - - private - - def construct_finder_arel(options = {}, scope = nil) - relation = options.is_a?(Hash) ? unscoped.apply_finder_options(options) : options - relation = scope.merge(relation) if scope - relation - end - end def populate_with_current_scope_attributes diff --git a/activerecord/lib/active_record/scoping/default.rb b/activerecord/lib/active_record/scoping/default.rb index 5f05d146f2..a2a85d4b96 100644 --- a/activerecord/lib/active_record/scoping/default.rb +++ b/activerecord/lib/active_record/scoping/default.rb @@ -1,4 +1,3 @@ -require 'active_support/concern' module ActiveRecord module Scoping @@ -7,7 +6,7 @@ module ActiveRecord included do # Stores the default scope for the class - config_attribute :default_scopes + class_attribute :default_scopes, instance_writer: false self.default_scopes = [] end @@ -30,14 +29,14 @@ module ActiveRecord # Post.limit(10) # Fires "SELECT * FROM posts LIMIT 10" # } # - # It is recommended to use the block form of unscoped because chaining - # unscoped with <tt>scope</tt> does not work. Assuming that + # It is recommended that you use the block form of unscoped because + # chaining unscoped with <tt>scope</tt> does not work. Assuming that # <tt>published</tt> is a <tt>scope</tt>, the following two statements - # are equal: the default_scope is applied on both. + # are equal: the <tt>default_scope</tt> is applied on both. # # Post.unscoped.published # Post.published - def unscoped #:nodoc: + def unscoped block_given? ? relation.scoping { yield } : relation end @@ -51,7 +50,7 @@ module ActiveRecord # the model. # # class Article < ActiveRecord::Base - # default_scope where(:published => true) + # default_scope { where(:published => true) } # end # # Article.all # => SELECT * FROM articles WHERE published = true @@ -62,12 +61,6 @@ module ActiveRecord # Article.new.published # => true # Article.create.published # => true # - # You can also use <tt>default_scope</tt> with a block, in order to have it lazily evaluated: - # - # class Article < ActiveRecord::Base - # default_scope { where(:published_at => Time.now - 1.week) } - # end - # # (You can also pass any object which responds to <tt>call</tt> to the <tt>default_scope</tt> # macro, and it will be called when building the default scope.) # @@ -75,8 +68,8 @@ module ActiveRecord # be merged together: # # class Article < ActiveRecord::Base - # default_scope where(:published => true) - # default_scope where(:rating => 'G') + # default_scope { where(:published => true) } + # default_scope { where(:rating => 'G') } # end # # Article.all # => SELECT * FROM articles WHERE published = true AND rating = 'G' @@ -92,21 +85,30 @@ module ActiveRecord # # Should return a scope, you can call 'super' here etc. # end # end - def default_scope(scope = {}) + def default_scope(scope = nil) scope = Proc.new if block_given? + + if scope.is_a?(Relation) || !scope.respond_to?(:call) + ActiveSupport::Deprecation.warn( + "Calling #default_scope without a block is deprecated. For example instead " \ + "of `default_scope where(color: 'red')`, please use " \ + "`default_scope { where(color: 'red') }`. (Alternatively you can just redefine " \ + "self.default_scope.)" + ) + end + self.default_scopes = default_scopes + [scope] end def build_default_scope #:nodoc: - if method(:default_scope).owner != ActiveRecord::Scoping::Default::ClassMethods + if !Base.is_a?(method(:default_scope).owner) + # The user has defined their own default scope method, so call that evaluate_default_scope { default_scope } elsif default_scopes.any? evaluate_default_scope do default_scopes.inject(relation) do |default_scope, scope| - if scope.is_a?(Hash) - default_scope.apply_finder_options(scope) - elsif !scope.is_a?(Relation) && scope.respond_to?(:call) - default_scope.merge(scope.call) + if !scope.is_a?(Relation) && scope.respond_to?(:call) + default_scope.merge(unscoped { scope.call }) else default_scope.merge(scope) end diff --git a/activerecord/lib/active_record/scoping/named.rb b/activerecord/lib/active_record/scoping/named.rb index 0edc3f1dcc..75f31229b5 100644 --- a/activerecord/lib/active_record/scoping/named.rb +++ b/activerecord/lib/active_record/scoping/named.rb @@ -1,8 +1,6 @@ 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 @@ -11,33 +9,25 @@ module ActiveRecord extend ActiveSupport::Concern module ClassMethods - # Returns an anonymous \scope. + # Returns an <tt>ActiveRecord::Relation</tt> scope object. # - # posts = Post.scoped + # posts = Post.all # posts.size # Fires "select count(*) from posts" and returns the count # posts.each {|p| puts p.name } # Fires "select * from posts" and loads post objects # - # fruits = Fruit.scoped + # fruits = Fruit.all # fruits = fruits.where(:color => 'red') if options[:red_only] # 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 - # objects is convenient. - # # You can define a \scope that applies to all finders using # ActiveRecord::Base.default_scope. - def scoped(options = nil) - if options - scoped.apply_finder_options(options) + def all + if current_scope + current_scope.clone else - if current_scope - current_scope.clone - else - scope = relation.clone - scope.default_scoped = true - scope - end + scope = relation + scope.default_scoped = true + scope end end @@ -48,7 +38,7 @@ module ActiveRecord if current_scope current_scope.scope_for_create else - scope = relation.clone + scope = relation scope.default_scoped = true scope.scope_for_create end @@ -171,29 +161,27 @@ module ActiveRecord # Article.published.featured.latest_article # Article.featured.titles - def scope(name, scope_options = {}) - name = name.to_sym - valid_scope_name?(name) - extension = Module.new(&Proc.new) if block_given? - - scope_proc = lambda do |*args| - options = scope_options.respond_to?(:call) ? unscoped { scope_options.call(*args) } : scope_options - options = scoped.apply_finder_options(options) if options.is_a?(Hash) - - relation = scoped.merge(options) + def scope(name, body, &block) + extension = Module.new(&block) if block - extension ? relation.extending(extension) : relation + # Check body.is_a?(Relation) to prevent the relation actually being + # loaded by respond_to? + if body.is_a?(Relation) || !body.respond_to?(:call) + ActiveSupport::Deprecation.warn( + "Using #scope without passing a callable object is deprecated. For " \ + "example `scope :red, where(color: 'red')` should be changed to " \ + "`scope :red, -> { where(color: 'red') }`. There are numerous gotchas " \ + "in the former usage and it makes the implementation more complicated " \ + "and buggy. (If you prefer, you can just define a class method named " \ + "`self.red`.)" + ) end - singleton_class.send(:redefine_method, name, &scope_proc) - end - - protected + singleton_class.send(:define_method, name) do |*args| + options = body.respond_to?(:call) ? unscoped { body.call(*args) } : body + relation = all.merge(options) - def valid_scope_name?(name) - if respond_to?(name, true) - logger.warn "Creating scope :#{name}. " \ - "Overwriting existing method #{self.name}.#{name}." + extension ? relation.extending(extension) : relation end end end diff --git a/activerecord/lib/active_record/serialization.rb b/activerecord/lib/active_record/serialization.rb index 41e3b92499..e8dd312a47 100644 --- a/activerecord/lib/active_record/serialization.rb +++ b/activerecord/lib/active_record/serialization.rb @@ -1,9 +1,21 @@ module ActiveRecord #:nodoc: + ActiveSupport.on_load(:active_record_config) do + mattr_accessor :include_root_in_json, instance_accessor: false + self.include_root_in_json = true + end + # = Active Record Serialization module Serialization extend ActiveSupport::Concern include ActiveModel::Serializers::JSON + included do + singleton_class.class_eval do + remove_method :include_root_in_json + delegate :include_root_in_json, to: 'ActiveRecord::Model' + end + end + def serializable_hash(options = nil) options = options.try(:clone) || {} diff --git a/activerecord/lib/active_record/serializers/xml_serializer.rb b/activerecord/lib/active_record/serializers/xml_serializer.rb index 2e60521638..834d01a1e8 100644 --- a/activerecord/lib/active_record/serializers/xml_serializer.rb +++ b/activerecord/lib/active_record/serializers/xml_serializer.rb @@ -18,8 +18,8 @@ module ActiveRecord #:nodoc: # <id type="integer">1</id> # <approved type="boolean">false</approved> # <replies-count type="integer">0</replies-count> - # <bonus-time type="datetime">2000-01-01T08:28:00+12:00</bonus-time> - # <written-on type="datetime">2003-07-16T09:28:00+1200</written-on> + # <bonus-time type="dateTime">2000-01-01T08:28:00+12:00</bonus-time> + # <written-on type="dateTime">2003-07-16T09:28:00+1200</written-on> # <content>Have a nice day</content> # <author-email-address>david@loudthinking.com</author-email-address> # <parent-id></parent-id> @@ -177,11 +177,6 @@ module ActiveRecord #:nodoc: end class XmlSerializer < ActiveModel::Serializers::Xml::Serializer #:nodoc: - def initialize(*args) - super - options[:except] = Array(options[:except]) | Array(@serializable.class.inheritance_column) - end - class Attribute < ActiveModel::Serializers::Xml::Serializer::Attribute #:nodoc: def compute_type klass = @serializable.class diff --git a/activerecord/lib/active_record/session_store.rb b/activerecord/lib/active_record/session_store.rb deleted file mode 100644 index ce43ae8066..0000000000 --- a/activerecord/lib/active_record/session_store.rb +++ /dev/null @@ -1,358 +0,0 @@ -module ActiveRecord - # = Active Record Session Store - # - # A session store backed by an Active Record class. A default class is - # provided, but any object duck-typing to an Active Record Session class - # with text +session_id+ and +data+ attributes is sufficient. - # - # The default assumes a +sessions+ tables with columns: - # +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, - # ActionController::SessionOverflowError will be raised. - # - # You may configure the table name, primary key, and data column. - # For example, at the end of <tt>config/application.rb</tt>: - # - # ActiveRecord::SessionStore::Session.table_name = 'legacy_session_table' - # ActiveRecord::SessionStore::Session.primary_key = 'session_id' - # ActiveRecord::SessionStore::Session.data_column_name = 'legacy_session_data' - # - # Note that setting the primary key to the +session_id+ frees you from - # having a separate +id+ column if you don't want it. However, you must - # set <tt>session.model.id = session.session_id</tt> by hand! A before filter - # on ApplicationController is a good place. - # - # Since the default class is a simple Active Record, you get timestamps - # for free if you add +created_at+ and +updated_at+ datetime columns to - # the +sessions+ table, making periodic session expiration a snap. - # - # You may provide your own session class implementation, whether a - # feature-packed Active Record or a bare-metal high-performance SQL - # store, by setting - # - # ActiveRecord::SessionStore.session_class = MySessionClass - # - # You must implement these methods: - # - # self.find_by_session_id(session_id) - # initialize(hash_of_session_id_and_data, options_hash = {}) - # attr_reader :session_id - # attr_accessor :data - # save - # destroy - # - # The example SqlBypass class is a generic SQL session store. You may - # use it as a basis for high-performance database-specific stores. - class SessionStore < ActionDispatch::Session::AbstractStore - module ClassMethods # :nodoc: - def marshal(data) - ::Base64.encode64(Marshal.dump(data)) if data - end - - def unmarshal(data) - Marshal.load(::Base64.decode64(data)) if data - end - - def drop_table! - connection.schema_cache.clear_table_cache!(table_name) - connection.drop_table table_name - end - - def create_table! - connection.schema_cache.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 - - # The default Active Record class. - class Session < ActiveRecord::Base - extend ClassMethods - - ## - # :singleton-method: - # Customizable data column name. Defaults to 'data'. - cattr_accessor :data_column_name - self.data_column_name = 'data' - - attr_accessible :session_id, :data, :marshaled_data - - before_save :marshal_data! - before_save :raise_on_session_data_overflow! - - class << self - def data_column_size_limit - @data_column_size_limit ||= columns_hash[data_column_name].limit - end - - # Hook to set up sessid compatibility. - def find_by_session_id(session_id) - setup_sessid_compatibility! - find_by_session_id(session_id) - end - - private - def session_id_column - 'session_id' - end - - # Compatibility with tables using sessid instead of session_id. - def setup_sessid_compatibility! - # Reset column info since it may be stale. - reset_column_information - if columns_hash['sessid'] - def self.find_by_session_id(*args) - find_by_sessid(*args) - end - - define_method(:session_id) { sessid } - define_method(:session_id=) { |session_id| self.sessid = session_id } - else - class << self; remove_possible_method :find_by_session_id; end - - def self.find_by_session_id(session_id) - find :first, :conditions => {:session_id=>session_id} - end - end - end - end - - def initialize(attributes = nil, options = {}) - @data = nil - super - end - - # Lazy-unmarshal session state. - def data - @data ||= self.class.unmarshal(read_attribute(@@data_column_name)) || {} - end - - attr_writer :data - - # Has the session been loaded yet? - def loaded? - @data - end - - private - def marshal_data! - return false unless loaded? - write_attribute(@@data_column_name, self.class.marshal(data)) - end - - # Ensures that the data about to be stored in the database is not - # larger than the data storage column. Raises - # ActionController::SessionOverflowError. - def raise_on_session_data_overflow! - return false unless loaded? - limit = self.class.data_column_size_limit - if limit and read_attribute(@@data_column_name).size > limit - raise ActionController::SessionOverflowError - end - end - end - - # A barebones session store which duck-types with the default session - # store but bypasses Active Record and issues SQL directly. This is - # an example session model class meant as a basis for your own classes. - # - # The database connection, table name, and session id and data columns - # are configurable class attributes. Marshaling and unmarshaling - # are implemented as class methods that you may override. By default, - # marshaling data is - # - # ::Base64.encode64(Marshal.dump(data)) - # - # and unmarshaling data is - # - # Marshal.load(::Base64.decode64(data)) - # - # This marshaling behavior is intended to store the widest range of - # binary session data in a +text+ column. For higher performance, - # store in a +blob+ column instead and forgo the Base64 encoding. - class SqlBypass - extend ClassMethods - - ## - # :singleton-method: - # The table name defaults to 'sessions'. - cattr_accessor :table_name - @@table_name = 'sessions' - - ## - # :singleton-method: - # The session id field defaults to 'session_id'. - cattr_accessor :session_id_column - @@session_id_column = 'session_id' - - ## - # :singleton-method: - # The data field defaults to 'data'. - cattr_accessor :data_column - @@data_column = 'data' - - class << self - alias :data_column_name :data_column - - # Use the ActiveRecord::Base.connection by default. - attr_writer :connection - - # Use the ActiveRecord::Base.connection_pool by default. - attr_writer :connection_pool - - def connection - @connection ||= ActiveRecord::Base.connection - end - - def connection_pool - @connection_pool ||= ActiveRecord::Base.connection_pool - end - - # Look up a session by id and unmarshal its data if found. - def find_by_session_id(session_id) - if record = connection.select_one("SELECT * FROM #{@@table_name} WHERE #{@@session_id_column}=#{connection.quote(session_id)}") - new(:session_id => session_id, :marshaled_data => record['data']) - end - end - end - - delegate :connection, :connection=, :connection_pool, :connection_pool=, :to => self - - attr_reader :session_id, :new_record - alias :new_record? :new_record - - attr_writer :data - - # Look for normal and marshaled data, self.find_by_session_id's way of - # telling us to postpone unmarshaling until the data is requested. - # We need to handle a normal data attribute in case of a new record. - def initialize(attributes) - @session_id = attributes[:session_id] - @data = attributes[:data] - @marshaled_data = attributes[:marshaled_data] - @new_record = @marshaled_data.nil? - end - - # Lazy-unmarshal session state. - def data - unless @data - if @marshaled_data - @data, @marshaled_data = self.class.unmarshal(@marshaled_data) || {}, nil - else - @data = {} - end - end - @data - end - - def loaded? - @data - end - - def save - return false unless loaded? - marshaled_data = self.class.marshal(data) - connect = connection - - if @new_record - @new_record = false - connect.update <<-end_sql, 'Create session' - INSERT INTO #{table_name} ( - #{connect.quote_column_name(session_id_column)}, - #{connect.quote_column_name(data_column)} ) - VALUES ( - #{connect.quote(session_id)}, - #{connect.quote(marshaled_data)} ) - end_sql - else - connect.update <<-end_sql, 'Update session' - UPDATE #{table_name} - SET #{connect.quote_column_name(data_column)}=#{connect.quote(marshaled_data)} - WHERE #{connect.quote_column_name(session_id_column)}=#{connect.quote(session_id)} - end_sql - end - end - - def destroy - return if @new_record - - connect = connection - connect.delete <<-end_sql, 'Destroy session' - DELETE FROM #{table_name} - WHERE #{connect.quote_column_name(session_id_column)}=#{connect.quote(session_id)} - end_sql - end - end - - # The class used for session storage. Defaults to - # ActiveRecord::SessionStore::Session - cattr_accessor :session_class - 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) - Base.silence do - unless sid and session = @@session_class.find_by_session_id(sid) - # If the sid was nil or if there is no pre-existing session under the sid, - # force the generation of a new sid and associate a new session associated with the new sid - sid = generate_sid - session = @@session_class.new(:session_id => sid, :data => {}) - end - env[SESSION_RECORD_KEY] = session - [sid, session.data] - end - end - - def set_session(env, sid, session_data, options) - Base.silence do - record = get_session_model(env, sid) - record.data = session_data - return false unless record.save - - session_data = record.data - if session_data && session_data.respond_to?(:each_value) - session_data.each_value do |obj| - obj.clear_association_cache if obj.respond_to?(:clear_association_cache) - end - end - end - - sid - end - - 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) - if env[ENV_SESSION_OPTIONS_KEY][:id].nil? - env[SESSION_RECORD_KEY] = find_session(sid) - else - env[SESSION_RECORD_KEY] ||= find_session(sid) - end - end - - def find_session(id) - @@session_class.find_by_session_id(id) || - @@session_class.new(:session_id => id, :data => {}) - end - end -end diff --git a/activerecord/lib/active_record/store.rb b/activerecord/lib/active_record/store.rb index 1c7b839e5e..df7f58c81f 100644 --- a/activerecord/lib/active_record/store.rb +++ b/activerecord/lib/active_record/store.rb @@ -1,6 +1,8 @@ +require 'active_support/core_ext/hash/indifferent_access' + module ActiveRecord # Store gives you a thin wrapper around serialize for the purpose of storing hashes in a single column. - # It's like a simple key/value store backed into your record when you don't care about being able to + # It's like a simple key/value store baked into your record when you don't care about being able to # query that store outside the context of a single record. # # You can then declare accessors to this store that are then accessible just like any other attribute @@ -10,42 +12,135 @@ module ActiveRecord # Make sure that you declare the database column used for the serialized store as a text, so there's # plenty of room. # + # You can set custom coder to encode/decode your serialized attributes to/from different formats. + # JSON, YAML, Marshal are supported out of the box. Generally it can be any wrapper that provides +load+ and +dump+. + # # Examples: # # class User < ActiveRecord::Base - # store :settings, accessors: [ :color, :homepage ] + # store :settings, accessors: [ :color, :homepage ], coder: JSON # end # # u = User.new(color: 'black', homepage: '37signals.com') # u.color # Accessor stored attribute # u.settings[:country] = 'Denmark' # Any attribute, even if not specified with an accessor # + # # There is no difference between strings and symbols for accessing custom attributes + # u.settings[:country] # => 'Denmark' + # u.settings['country'] # => 'Denmark' + # # # Add additional accessors to an existing store through store_accessor # class SuperUser < User # store_accessor :settings, :privileges, :servants # end + # + # The stored attribute names can be retrieved using +stored_attributes+. + # + # User.stored_attributes[:settings] # [:color, :homepage] + # + # == Overwriting default accessors + # + # All stored values are automatically available through 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_store_attribute(store_attribute_name, attr_name)</tt> and + # <tt>write_store_attribute(store_attribute_name, attr_name, value)</tt> to actually + # change things. + # + # class Song < ActiveRecord::Base + # # Uses a stored integer to hold the volume adjustment of the song + # store :settings, accessors: [:volume_adjustment] + # + # def volume_adjustment=(decibels) + # write_store_attribute(:settings, :volume_adjustment, decibels.to_i) + # end + # + # def volume_adjustment + # read_store_attribute(:settings, :volume_adjustment).to_i + # end + # end module Store extend ActiveSupport::Concern + included do + class_attribute :stored_attributes, instance_accessor: false + self.stored_attributes = {} + end + module ClassMethods def store(store_attribute, options = {}) - serialize store_attribute, Hash + serialize store_attribute, IndifferentCoder.new(options[:coder]) store_accessor(store_attribute, options[:accessors]) if options.has_key? :accessors end def store_accessor(store_attribute, *keys) - keys.flatten.each do |key| + keys = keys.flatten + keys.each do |key| define_method("#{key}=") do |value| - send("#{store_attribute}=", {}) unless send(store_attribute).is_a?(Hash) - send(store_attribute)[key] = value - send("#{store_attribute}_will_change!") + write_store_attribute(store_attribute, key, value) end define_method(key) do - send("#{store_attribute}=", {}) unless send(store_attribute).is_a?(Hash) - send(store_attribute)[key] + read_store_attribute(store_attribute, key) end end + + self.stored_attributes[store_attribute] ||= [] + self.stored_attributes[store_attribute] |= keys + end + end + + protected + def read_store_attribute(store_attribute, key) + attribute = initialize_store_attribute(store_attribute) + attribute[key] + end + + def write_store_attribute(store_attribute, key, value) + attribute = initialize_store_attribute(store_attribute) + if value != attribute[key] + send :"#{store_attribute}_will_change!" + attribute[key] = value + end + end + + private + def initialize_store_attribute(store_attribute) + attribute = send(store_attribute) + unless attribute.is_a?(HashWithIndifferentAccess) + attribute = IndifferentCoder.as_indifferent_hash(attribute) + send :"#{store_attribute}=", attribute + end + attribute + end + + class IndifferentCoder # :nodoc: + def initialize(coder_or_class_name) + @coder = + if coder_or_class_name.respond_to?(:load) && coder_or_class_name.respond_to?(:dump) + coder_or_class_name + else + ActiveRecord::Coders::YAMLColumn.new(coder_or_class_name || Object) + end + end + + def dump(obj) + @coder.dump self.class.as_indifferent_hash(obj) + end + + def load(yaml) + self.class.as_indifferent_hash @coder.load(yaml) + end + + def self.as_indifferent_hash(obj) + case obj + when HashWithIndifferentAccess + obj + when Hash + obj.with_indifferent_access + else + HashWithIndifferentAccess.new + end end end end diff --git a/activerecord/lib/active_record/tasks/database_tasks.rb b/activerecord/lib/active_record/tasks/database_tasks.rb new file mode 100644 index 0000000000..fda51b3d76 --- /dev/null +++ b/activerecord/lib/active_record/tasks/database_tasks.rb @@ -0,0 +1,150 @@ +module ActiveRecord + module Tasks # :nodoc: + module DatabaseTasks # :nodoc: + extend self + + attr_writer :current_config + + LOCAL_HOSTS = ['127.0.0.1', 'localhost'] + + def register_task(pattern, task) + @tasks ||= {} + @tasks[pattern] = task + end + + register_task(/mysql/, ActiveRecord::Tasks::MySQLDatabaseTasks) + register_task(/postgresql/, ActiveRecord::Tasks::PostgreSQLDatabaseTasks) + register_task(/sqlite/, ActiveRecord::Tasks::SQLiteDatabaseTasks) + + def current_config(options = {}) + options.reverse_merge! :env => Rails.env + if options.has_key?(:config) + @current_config = options[:config] + else + @current_config ||= if ENV['DATABASE_URL'] + database_url_config + else + ActiveRecord::Base.configurations[options[:env]] + end + end + end + + def create(*arguments) + configuration = arguments.first + class_for_adapter(configuration['adapter']).new(*arguments).create + rescue Exception => error + $stderr.puts error, *(error.backtrace) + $stderr.puts "Couldn't create database for #{configuration.inspect}" + end + + def create_all + each_local_configuration { |configuration| create configuration } + end + + def create_current(environment = Rails.env) + each_current_configuration(environment) { |configuration| + create configuration + } + ActiveRecord::Base.establish_connection environment + end + + def create_database_url + create database_url_config + end + + def drop(*arguments) + configuration = arguments.first + class_for_adapter(configuration['adapter']).new(*arguments).drop + rescue Exception => error + $stderr.puts error, *(error.backtrace) + $stderr.puts "Couldn't drop #{configuration['database']}" + end + + def drop_all + each_local_configuration { |configuration| drop configuration } + end + + def drop_current(environment = Rails.env) + each_current_configuration(environment) { |configuration| + drop configuration + } + end + + def drop_database_url + drop database_url_config + end + + def charset_current(environment = Rails.env) + charset ActiveRecord::Base.configurations[environment] + end + + def charset(*arguments) + configuration = arguments.first + class_for_adapter(configuration['adapter']).new(*arguments).charset + end + + def collation_current(environment = Rails.env) + collation ActiveRecord::Base.configurations[environment] + end + + def collation(*arguments) + configuration = arguments.first + class_for_adapter(configuration['adapter']).new(*arguments).collation + end + + def purge(configuration) + class_for_adapter(configuration['adapter']).new(configuration).purge + end + + def structure_dump(*arguments) + configuration = arguments.first + filename = arguments.delete_at 1 + class_for_adapter(configuration['adapter']).new(*arguments).structure_dump(filename) + end + + def structure_load(*arguments) + configuration = arguments.first + filename = arguments.delete_at 1 + class_for_adapter(configuration['adapter']).new(*arguments).structure_load(filename) + end + + private + + def database_url_config + @database_url_config ||= + ConnectionAdapters::ConnectionSpecification::Resolver.new(ENV["DATABASE_URL"], {}).spec.config.stringify_keys + end + + def class_for_adapter(adapter) + key = @tasks.keys.detect { |pattern| adapter[pattern] } + @tasks[key] + end + + def each_current_configuration(environment) + environments = [environment] + environments << 'test' if environment.development? + + configurations = ActiveRecord::Base.configurations.values_at(*environments) + configurations.compact.each do |configuration| + yield configuration unless configuration['database'].blank? + end + end + + def each_local_configuration + ActiveRecord::Base.configurations.each_value do |configuration| + next unless configuration['database'] + + if local_database?(configuration) + yield configuration + else + $stderr.puts "This task only modifies local databases. #{configuration['database']} is on a remote host." + end + end + end + + def local_database?(configuration) + configuration['host'].blank? || LOCAL_HOSTS.include?(configuration['host']) + end + end + end +end diff --git a/activerecord/lib/active_record/tasks/mysql_database_tasks.rb b/activerecord/lib/active_record/tasks/mysql_database_tasks.rb new file mode 100644 index 0000000000..2340f949b7 --- /dev/null +++ b/activerecord/lib/active_record/tasks/mysql_database_tasks.rb @@ -0,0 +1,128 @@ +module ActiveRecord + module Tasks # :nodoc: + class MySQLDatabaseTasks # :nodoc: + + DEFAULT_CHARSET = ENV['CHARSET'] || 'utf8' + DEFAULT_COLLATION = ENV['COLLATION'] || 'utf8_unicode_ci' + ACCESS_DENIED_ERROR = 1045 + + delegate :connection, :establish_connection, to: ActiveRecord::Base + + def initialize(configuration) + @configuration = configuration + end + + def create + establish_connection configuration_without_database + connection.create_database configuration['database'], creation_options + establish_connection configuration + rescue error_class => error + raise error unless error.errno == ACCESS_DENIED_ERROR + + $stdout.print error.error + establish_connection root_configuration_without_database + connection.create_database configuration['database'], creation_options + connection.execute grant_statement.gsub(/\s+/, ' ').strip + establish_connection configuration + rescue error_class => error + $stderr.puts error.error + $stderr.puts "Couldn't create database for #{configuration.inspect}, #{creation_options.inspect}" + $stderr.puts "(If you set the charset manually, make sure you have a matching collation)" if configuration['encoding'] + end + + def drop + establish_connection configuration + connection.drop_database configuration['database'] + end + + def purge + establish_connection :test + connection.recreate_database configuration['database'], creation_options + end + + def charset + connection.charset + end + + def collation + connection.collation + end + + def structure_dump(filename) + args = prepare_command_options('mysqldump') + args.concat(["--result-file", "#{filename}"]) + args.concat(["--no-data"]) + args.concat(["#{configuration['database']}"]) + Kernel.system(*args) + end + + def structure_load(filename) + args = prepare_command_options('mysql') + args.concat(['--execute', %{SET FOREIGN_KEY_CHECKS = 0; SOURCE #{filename}; SET FOREIGN_KEY_CHECKS = 1}]) + args.concat(["--database", "#{configuration['database']}"]) + Kernel.system(*args) + end + + private + + def configuration + @configuration + end + + def configuration_without_database + configuration.merge('database' => nil) + end + + def creation_options + { + charset: (configuration['encoding'] || DEFAULT_CHARSET), + collation: (configuration['collation'] || DEFAULT_COLLATION) + } + end + + def error_class + case configuration['adapter'] + when /jdbc/ + require 'active_record/railties/jdbcmysql_error' + ArJdbcMySQL::Error + when /mysql2/ + Mysql2::Error + else + Mysql::Error + end + end + + def grant_statement + <<-SQL +GRANT ALL PRIVILEGES ON #{configuration['database']}.* + TO '#{configuration['username']}'@'localhost' +IDENTIFIED BY '#{configuration['password']}' WITH GRANT OPTION; + SQL + end + + def root_configuration_without_database + configuration_without_database.merge( + 'username' => 'root', + 'password' => root_password + ) + end + + def root_password + $stdout.print "Please provide the root password for your mysql installation\n>" + $stdin.gets.strip + end + + def prepare_command_options(command) + args = [command] + args.concat(['--user', configuration['username']]) if configuration['username'] + args << "--password=#{configuration['password']}" if configuration['password'] + args.concat(['--default-character-set', configuration['encoding']]) if configuration['encoding'] + configuration.slice('host', 'port', 'socket').each do |k, v| + args.concat([ "--#{k}", v ]) if v + end + args + end + + end + end +end diff --git a/activerecord/lib/active_record/tasks/postgresql_database_tasks.rb b/activerecord/lib/active_record/tasks/postgresql_database_tasks.rb new file mode 100644 index 0000000000..ea5cb888fb --- /dev/null +++ b/activerecord/lib/active_record/tasks/postgresql_database_tasks.rb @@ -0,0 +1,85 @@ +require 'shellwords' + +module ActiveRecord + module Tasks # :nodoc: + class PostgreSQLDatabaseTasks # :nodoc: + + DEFAULT_ENCODING = ENV['CHARSET'] || 'utf8' + + delegate :connection, :establish_connection, :clear_active_connections!, + to: ActiveRecord::Base + + def initialize(configuration) + @configuration = configuration + end + + def create(master_established = false) + establish_master_connection unless master_established + connection.create_database configuration['database'], + configuration.merge('encoding' => encoding) + establish_connection configuration + end + + def drop + establish_master_connection + connection.drop_database configuration['database'] + end + + def charset + connection.encoding + end + + def collation + connection.collation + end + + def purge + clear_active_connections! + drop + create true + end + + def structure_dump(filename) + set_psql_env + search_path = configuration['schema_search_path'] + unless search_path.blank? + search_path = search_path.split(",").map{|search_path_part| "--schema=#{Shellwords.escape(search_path_part.strip)}" }.join(" ") + end + + command = "pg_dump -i -s -x -O -f #{Shellwords.escape(filename)} #{search_path} #{Shellwords.escape(configuration['database'])}" + raise 'Error dumping database' unless Kernel.system(command) + + File.open(filename, "a") { |f| f << "SET search_path TO #{ActiveRecord::Base.connection.schema_search_path};\n\n" } + end + + def structure_load(filename) + set_psql_env + Kernel.system("psql -f #{filename} #{configuration['database']}") + end + + private + + def configuration + @configuration + end + + def encoding + configuration['encoding'] || DEFAULT_ENCODING + end + + def establish_master_connection + establish_connection configuration.merge( + 'database' => 'postgres', + 'schema_search_path' => 'public' + ) + end + + def set_psql_env + ENV['PGHOST'] = configuration['host'] if configuration['host'] + ENV['PGPORT'] = configuration['port'].to_s if configuration['port'] + ENV['PGPASSWORD'] = configuration['password'].to_s if configuration['password'] + ENV['PGUSER'] = configuration['username'].to_s if configuration['username'] + end + end + end +end diff --git a/activerecord/lib/active_record/tasks/sqlite_database_tasks.rb b/activerecord/lib/active_record/tasks/sqlite_database_tasks.rb new file mode 100644 index 0000000000..da01058a82 --- /dev/null +++ b/activerecord/lib/active_record/tasks/sqlite_database_tasks.rb @@ -0,0 +1,55 @@ +module ActiveRecord + module Tasks # :nodoc: + class SQLiteDatabaseTasks # :nodoc: + + delegate :connection, :establish_connection, to: ActiveRecord::Base + + def initialize(configuration, root = Rails.root) + @configuration, @root = configuration, root + end + + def create + if File.exist?(configuration['database']) + $stderr.puts "#{configuration['database']} already exists" + return + end + + establish_connection configuration + connection + end + + def drop + require 'pathname' + path = Pathname.new configuration['database'] + file = path.absolute? ? path.to_s : File.join(root, path) + + FileUtils.rm(file) if File.exist?(file) + end + alias :purge :drop + + def charset + connection.encoding + end + + def structure_dump(filename) + dbfile = configuration['database'] + `sqlite3 #{dbfile} .schema > #{filename}` + end + + def structure_load(filename) + dbfile = configuration['database'] + `sqlite3 #{dbfile} < "#{filename}"` + end + + private + + def configuration + @configuration + end + + def root + @root + end + end + end +end diff --git a/activerecord/lib/active_record/test_case.rb b/activerecord/lib/active_record/test_case.rb index 4d881f0f7d..c035ad43a2 100644 --- a/activerecord/lib/active_record/test_case.rb +++ b/activerecord/lib/active_record/test_case.rb @@ -1,4 +1,3 @@ -require 'active_support/deprecation' require 'active_support/test_case' ActiveSupport::Deprecation.warn('ActiveRecord::TestCase is deprecated, please use ActiveSupport::TestCase') @@ -7,18 +6,8 @@ module ActiveRecord # # 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 teardown - SQLCounter.log.clear - end - - def cleanup_identity_map - ActiveRecord::IdentityMap.clear + SQLCounter.clear_log end def assert_date_from_db(expected, actual, message = nil) @@ -32,47 +21,57 @@ module ActiveRecord end def assert_sql(*patterns_to_match) - SQLCounter.log = [] + SQLCounter.clear_log yield - SQLCounter.log + SQLCounter.log_all ensure failed_patterns = [] patterns_to_match.each do |pattern| - failed_patterns << pattern unless SQLCounter.log.any?{ |sql| pattern === sql } + failed_patterns << pattern unless SQLCounter.log_all.any?{ |sql| pattern === sql } end assert failed_patterns.empty?, "Query pattern(s) #{failed_patterns.map{ |p| p.inspect }.join(', ')} not found.#{SQLCounter.log.size == 0 ? '' : "\nQueries:\n#{SQLCounter.log.join("\n")}"}" end - def assert_queries(num = 1) - SQLCounter.log = [] + def assert_queries(num = 1, options = {}) + ignore_none = options.fetch(:ignore_none) { num == :any } + SQLCounter.clear_log yield ensure - assert_equal num, SQLCounter.log.size, "#{SQLCounter.log.size} instead of #{num} queries were executed.#{SQLCounter.log.size == 0 ? '' : "\nQueries:\n#{SQLCounter.log.join("\n")}"}" + the_log = ignore_none ? SQLCounter.log_all : SQLCounter.log + if num == :any + assert_operator the_log.size, :>=, 1, "1 or more queries expected, but none were executed." + else + mesg = "#{the_log.size} instead of #{num} queries were executed.#{the_log.size == 0 ? '' : "\nQueries:\n#{the_log.join("\n")}"}" + assert_equal num, the_log.size, mesg + end end def assert_no_queries(&block) - prev_ignored_sql = SQLCounter.ignored_sql - SQLCounter.ignored_sql = [] - assert_queries(0, &block) - ensure - SQLCounter.ignored_sql = prev_ignored_sql + assert_queries(0, :ignore_none => true, &block) end end class SQLCounter class << self - attr_accessor :ignored_sql, :log + attr_accessor :ignored_sql, :log, :log_all + def clear_log; self.log = []; self.log_all = []; end end - self.log = [] + self.clear_log self.ignored_sql = [/^PRAGMA (?!(table_info))/, /^SELECT currval/, /^SELECT CAST/, /^SELECT @@IDENTITY/, /^SELECT @@ROWCOUNT/, /^SAVEPOINT/, /^ROLLBACK TO SAVEPOINT/, /^RELEASE SAVEPOINT/, /^SHOW max_identifier_length/, /^BEGIN/, /^COMMIT/] # FIXME: this needs to be refactored so specific database can add their own - # ignored SQL. This ignored SQL is for Oracle. - ignored_sql.concat [/^select .*nextval/i, /^SAVEPOINT/, /^ROLLBACK TO/, /^\s*select .* from all_triggers/im] - + # ignored SQL, or better yet, use a different notification for the queries + # instead examining the SQL content. + oracle_ignored = [/^select .*nextval/i, /^SAVEPOINT/, /^ROLLBACK TO/, /^\s*select .* from all_triggers/im] + mysql_ignored = [/^SHOW TABLES/i, /^SHOW FULL FIELDS/] + postgresql_ignored = [/^\s*select\b.*\bfrom\b.*pg_namespace\b/im, /^\s*select\b.*\battname\b.*\bfrom\b.*\bpg_attribute\b/im] + + [oracle_ignored, mysql_ignored, postgresql_ignored].each do |db_ignored_sql| + ignored_sql.concat db_ignored_sql + end attr_reader :ignore @@ -85,8 +84,10 @@ module ActiveRecord # FIXME: this seems bad. we should probably have a better way to indicate # the query was cached - return if 'CACHE' == values[:name] || ignore =~ sql - self.class.log << sql + return if 'CACHE' == values[:name] + + self.class.log_all << sql + self.class.log << sql unless ignore =~ sql end end diff --git a/activerecord/lib/active_record/timestamp.rb b/activerecord/lib/active_record/timestamp.rb index c717fdea47..c32e0d6bf8 100644 --- a/activerecord/lib/active_record/timestamp.rb +++ b/activerecord/lib/active_record/timestamp.rb @@ -1,6 +1,10 @@ -require 'active_support/core_ext/class/attribute' module ActiveRecord + ActiveSupport.on_load(:active_record_config) do + mattr_accessor :record_timestamps, instance_accessor: false + self.record_timestamps = true + end + # = Active Record Timestamp # # Active Record automatically timestamps create and update operations if the @@ -33,8 +37,7 @@ module ActiveRecord extend ActiveSupport::Concern included do - config_attribute :record_timestamps, :instance_writer => true - self.record_timestamps = true + config_attribute :record_timestamps, instance_writer: true end def initialize_dup(other) diff --git a/activerecord/lib/active_record/transactions.rb b/activerecord/lib/active_record/transactions.rb index b492377d18..934393b4e7 100644 --- a/activerecord/lib/active_record/transactions.rb +++ b/activerecord/lib/active_record/transactions.rb @@ -165,7 +165,7 @@ module ActiveRecord # 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 on MySQL and PostgreSQL. See - # http://dev.mysql.com/doc/refman/5.0/en/savepoint.html + # http://dev.mysql.com/doc/refman/5.6/en/savepoint.html # for more information about savepoints. # # === Callbacks @@ -208,6 +208,21 @@ module ActiveRecord connection.transaction(options, &block) end + # This callback is called after a record has been created, updated, or destroyed. + # + # You can specify that the callback should only be fired by a certain action with + # the +:on+ option: + # + # after_commit :do_foo, :on => :create + # after_commit :do_bar, :on => :update + # after_commit :do_baz, :on => :destroy + # + # Also, to have the callback fired on create and update, but not on destroy: + # + # after_commit :do_zoo, :if => :persisted? + # + # Note that transactional fixtures do not play well with this feature. Please + # use the +test_after_commit+ gem to have these hooks fired in tests. def after_commit(*args, &block) options = args.last if options.is_a?(Hash) && options[:on] @@ -217,6 +232,9 @@ module ActiveRecord set_callback(:commit, :after, *args, &block) end + # This callback is called after a create, update, or destroy are rolled back. + # + # Please check the documentation of +after_commit+ for options. def after_rollback(*args, &block) options = args.last if options.is_a?(Hash) && options[:on] @@ -251,7 +269,6 @@ module ActiveRecord remember_transaction_record_state yield rescue Exception - IdentityMap.remove(self) if IdentityMap.enabled? restore_transaction_record_state raise ensure @@ -270,7 +287,6 @@ module ActiveRecord def rolledback!(force_restore_state = false) #:nodoc: run_callbacks :rollback ensure - IdentityMap.remove(self) if IdentityMap.enabled? restore_transaction_record_state(force_restore_state) end @@ -292,7 +308,13 @@ module ActiveRecord status = nil self.class.transaction do add_to_transaction - status = yield + begin + status = yield + rescue ActiveRecord::Rollback + @_start_transaction_state[:level] = (@_start_transaction_state[:level] || 0) - 1 + status = nil + end + raise ActiveRecord::Rollback unless status end status @@ -302,32 +324,26 @@ 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[:new_record] = @new_record - end - unless @_start_transaction_state.include?(:destroyed) - @_start_transaction_state[:destroyed] = @destroyed - end + @_start_transaction_state[:new_record] = @new_record + @_start_transaction_state[:destroyed] = @destroyed @_start_transaction_state[:level] = (@_start_transaction_state[:level] || 0) + 1 end # Clear the new record state and id of a record. def clear_transaction_record_state #:nodoc: - if defined?(@_start_transaction_state) - @_start_transaction_state[:level] = (@_start_transaction_state[:level] || 0) - 1 - remove_instance_variable(:@_start_transaction_state) if @_start_transaction_state[:level] < 1 - end + @_start_transaction_state[:level] = (@_start_transaction_state[:level] || 0) - 1 + @_start_transaction_state.clear if @_start_transaction_state[:level] < 1 end # Restore the new record state and id of a record that was previously saved by a call to save_record_state. def restore_transaction_record_state(force = false) #:nodoc: - if defined?(@_start_transaction_state) + unless @_start_transaction_state.empty? @_start_transaction_state[:level] = (@_start_transaction_state[:level] || 0) - 1 - if @_start_transaction_state[:level] < 1 - restore_state = remove_instance_variable(:@_start_transaction_state) - @attributes = @attributes.dup if @attributes.frozen? + if @_start_transaction_state[:level] < 1 || force + restore_state = @_start_transaction_state + was_frozen = @attributes.frozen? + @attributes = @attributes.dup if was_frozen @new_record = restore_state[:new_record] @destroyed = restore_state[:destroyed] if restore_state.has_key?(:id) @@ -336,13 +352,15 @@ module ActiveRecord @attributes.delete(self.class.primary_key) @attributes_cache.delete(self.class.primary_key) end + @attributes.freeze if was_frozen + @_start_transaction_state.clear end end end # Determine if a record was created or destroyed in a transaction. State should be one of :new_record or :destroyed. def transaction_record_state(state) #:nodoc: - @_start_transaction_state[state] if defined?(@_start_transaction_state) + @_start_transaction_state[state] end # Determine if a transaction included an action for :create, :update, or :destroy. Used in filtering callbacks. diff --git a/activerecord/lib/active_record/validations.rb b/activerecord/lib/active_record/validations.rb index 4b075183c3..ed561bfb3c 100644 --- a/activerecord/lib/active_record/validations.rb +++ b/activerecord/lib/active_record/validations.rb @@ -14,7 +14,7 @@ module ActiveRecord def initialize(record) @record = record errors = @record.errors.full_messages.join(", ") - super(I18n.t("activerecord.errors.messages.record_invalid", :errors => errors)) + super(I18n.t(:"#{@record.class.i18n_scope}.errors.messages.record_invalid", :errors => errors, :default => :"errors.messages.record_invalid")) end end @@ -32,11 +32,11 @@ module ActiveRecord module ClassMethods # Creates an object just like Base.create but calls <tt>save!</tt> instead of +save+ # so an exception is raised if the record is invalid. - def create!(attributes = nil, options = {}, &block) + def create!(attributes = nil, &block) if attributes.is_a?(Array) - attributes.collect { |attr| create!(attr, options, &block) } + attributes.collect { |attr| create!(attr, &block) } else - object = new(attributes, options) + object = new(attributes) yield(object) if block_given? object.save! object @@ -81,3 +81,4 @@ end require "active_record/validations/associated" require "active_record/validations/uniqueness" +require "active_record/validations/presence" diff --git a/activerecord/lib/active_record/validations/associated.rb b/activerecord/lib/active_record/validations/associated.rb index afce149da9..1fa6629980 100644 --- a/activerecord/lib/active_record/validations/associated.rb +++ b/activerecord/lib/active_record/validations/associated.rb @@ -1,6 +1,6 @@ module ActiveRecord module Validations - class AssociatedValidator < ActiveModel::EachValidator + class AssociatedValidator < ActiveModel::EachValidator #:nodoc: def validate_each(record, attribute, value) if Array.wrap(value).reject {|r| r.marked_for_destruction? || r.valid?(record.validation_context) }.any? record.errors.add(attribute, :invalid, options.merge(:value => value)) @@ -9,7 +9,8 @@ module ActiveRecord end module ClassMethods - # Validates whether the associated object or objects are all valid themselves. Works with any kind of association. + # Validates whether the associated object or objects are all valid + # themselves. Works with any kind of association. # # class Book < ActiveRecord::Base # has_many :pages @@ -18,23 +19,28 @@ module ActiveRecord # validates_associated :pages, :library # end # - # WARNING: This validation must not be used on both ends of an association. Doing so will lead to a circular dependency and cause infinite recursion. + # WARNING: This validation must not be used on both ends of an association. + # Doing so will lead to 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 - # use +validates_presence_of+. + # 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>:message</tt> - A custom error message (default is: "is invalid"). # * <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. - # * <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 return or evaluate to a true or false value. + # * <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. + # * <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 return or evaluate to a +true+ or +false+ + # value. def validates_associated(*attr_names) validates_with AssociatedValidator, _merge_attributes(attr_names) end diff --git a/activerecord/lib/active_record/validations/presence.rb b/activerecord/lib/active_record/validations/presence.rb new file mode 100644 index 0000000000..056527b512 --- /dev/null +++ b/activerecord/lib/active_record/validations/presence.rb @@ -0,0 +1,64 @@ +module ActiveRecord + module Validations + class PresenceValidator < ActiveModel::Validations::PresenceValidator + def validate(record) + super + attributes.each do |attribute| + next unless record.class.reflect_on_association(attribute) + value = record.send(attribute) + if Array(value).all? { |r| r.marked_for_destruction? } + record.errors.add(attribute, :blank, options) + end + end + end + end + + module ClassMethods + # Validates that the specified attributes are not blank (as defined by + # Object#blank?), and, if the attribute is an association, that the + # associated object is not marked for destruction. Happens by default + # on save. + # + # class Person < ActiveRecord::Base + # has_one :face + # validates_presence_of :face + # end + # + # The face attribute must be in the object and it cannot be blank or marked + # for destruction. + # + # If you want to validate the presence of a boolean field (where the real values + # are true and false), you will want to use + # <tt>validates_inclusion_of :field_name, :in => [true, false]</tt>. + # + # This is due to the way Object#blank? handles boolean values: + # <tt>false.blank? # => true</tt>. + # + # This validator defers to the ActiveModel validation for presence, adding the + # check to see that an associated object is not marked for destruction. This + # prevents the parent object from validating successfully and saving, which then + # deletes the associated object, thus putting the parent object into an invalid + # state. + # + # Configuration options: + # * <tt>:message</tt> - A custom error message (default is: "can't be blank"). + # * <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. + # * <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 return or evaluate to a true or false value. + # * <tt>:strict</tt> - Specifies whether validation should be strict. + # See <tt>ActiveModel::Validation#validates!</tt> for more information. + # + def validates_presence_of(*attr_names) + validates_with PresenceValidator, _merge_attributes(attr_names) + end + end + end +end diff --git a/activerecord/lib/active_record/validations/uniqueness.rb b/activerecord/lib/active_record/validations/uniqueness.rb index 9556878f63..c117872ac8 100644 --- a/activerecord/lib/active_record/validations/uniqueness.rb +++ b/activerecord/lib/active_record/validations/uniqueness.rb @@ -2,7 +2,7 @@ require 'active_support/core_ext/array/prepend_and_append' module ActiveRecord module Validations - class UniquenessValidator < ActiveModel::EachValidator + class UniquenessValidator < ActiveModel::EachValidator #:nodoc: def initialize(options) super(options.reverse_merge(:case_sensitive => true)) end @@ -26,7 +26,7 @@ module ActiveRecord relation = relation.and(table[finder_class.primary_key.to_sym].not_eq(record.send(:id))) if record.persisted? Array(options[:scope]).each do |scope_item| - scope_value = record.send(scope_item) + scope_value = record.read_attribute(scope_item) reflection = record.class.reflect_on_association(scope_item) if reflection scope_value = record.send(reflection.foreign_key) @@ -35,8 +35,14 @@ module ActiveRecord relation = relation.and(table[scope_item].eq(scope_value)) end - if finder_class.unscoped.where(relation).exists? - record.errors.add(attribute, :taken, options.except(:case_sensitive, :scope).merge(:value => value)) + relation = finder_class.unscoped.where(relation) + + if options[:conditions] + relation = relation.merge(options[:conditions]) + end + + if relation.exists? + record.errors.add(attribute, :taken, options.except(:case_sensitive, :scope, :conditions).merge(:value => value)) end end @@ -81,44 +87,67 @@ module ActiveRecord end module ClassMethods - # Validates whether the value of the specified attributes are unique across the system. - # Useful for making sure that only one user + # 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 # end # - # It can also validate whether the value of the specified attributes are unique based on a scope parameter: + # It can also validate whether the value of the specified attributes are + # unique based on a <tt>:scope</tt> parameter: # # class Person < ActiveRecord::Base - # validates_uniqueness_of :user_name, :scope => :account_id + # 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. + # 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] + # validates_uniqueness_of :teacher_id, scope: [:semester_id, :class_id] + # end + # + # It is also possible to limit the uniqueness constraint to a set of + # records matching certain conditions. In this example archived articles + # are not being taken into consideration when validating uniqueness + # of the title attribute: + # + # class Article < ActiveRecord::Base + # validates_uniqueness_of :title, conditions: where('status != ?', 'archived') # 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: - # * <tt>:message</tt> - Specifies a custom error message (default is: "has already been taken"). - # * <tt>:scope</tt> - One or more columns by which to limit the scope of the uniqueness constraint. - # * <tt>:case_sensitive</tt> - Looks for an exact match. Ignored by non-text columns (+true+ by default). - # * <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>). - # 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 - # return or evaluate to a true or false value. + # + # * <tt>:message</tt> - Specifies a custom error message (default is: + # "has already been taken"). + # * <tt>:scope</tt> - One or more columns by which to limit the scope of + # the uniqueness constraint. + # * <tt>:conditions</tt> - Specify the conditions to be included as a + # <tt>WHERE</tt> SQL fragment to limit the uniqueness constraint lookup + # (e.g. <tt>conditions: where('status = ?', 'active')</tt>). + # * <tt>:case_sensitive</tt> - Looks for an exact match. Ignored by + # non-text columns (+true+ by default). + # * <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>). 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 ot 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 # @@ -174,16 +203,16 @@ module ActiveRecord # # 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. + # 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) end |