diff options
Diffstat (limited to 'activerecord/lib')
113 files changed, 1751 insertions, 1305 deletions
diff --git a/activerecord/lib/active_record/aggregations.rb b/activerecord/lib/active_record/aggregations.rb index a2aea63bdd..be88c7c9e8 100644 --- a/activerecord/lib/active_record/aggregations.rb +++ b/activerecord/lib/active_record/aggregations.rb @@ -1,6 +1,6 @@ module ActiveRecord - # = Active Record Aggregations - module Aggregations # :nodoc: + # See ActiveRecord::Aggregations::ClassMethods for documentation + module Aggregations extend ActiveSupport::Concern def initialize_dup(*) # :nodoc: @@ -24,7 +24,7 @@ module ActiveRecord super end - # Active Record implements aggregation through a macro-like class method called +composed_of+ + # 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 @@ -120,12 +120,12 @@ module ActiveRecord # # 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. The - # Money#exchange_to method is an example of this. It returns a new value object instead of changing + # <tt>Money#exchange_to</tt> 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. # # The immutable requirement is enforced by Active Record by freezing any object assigned as a value - # object. Attempting to change it afterwards will result in a RuntimeError. + # object. Attempting to change it afterwards will result in a +RuntimeError+. # # Read more about value objects on http://c2.com/cgi/wiki?ValueObject and on the dangers of not # keeping value objects immutable on http://c2.com/cgi/wiki?ValueObjectsShouldBeImmutable @@ -134,17 +134,17 @@ module ActiveRecord # # By default value objects are initialized by calling the <tt>new</tt> constructor of the value # class passing each of the mapped attributes, in the order specified by the <tt>:mapping</tt> - # option, as arguments. If the value class doesn't support this convention then +composed_of+ allows + # option, as arguments. If the value class doesn't support this convention then #composed_of allows # a custom constructor to be specified. # # When a new value is assigned to the value object, the default assumption is that the new value # is an instance of the value class. Specifying a custom converter allows the new value to be automatically # converted to an instance of value class if necessary. # - # For example, the NetworkResource model has +network_address+ and +cidr_range+ attributes that should be - # aggregated using the NetAddr::CIDR value class (http://www.rubydoc.info/gems/netaddr/1.5.0/NetAddr/CIDR). + # For example, the +NetworkResource+ model has +network_address+ and +cidr_range+ attributes that should be + # aggregated using the +NetAddr::CIDR+ value class (http://www.rubydoc.info/gems/netaddr/1.5.0/NetAddr/CIDR). # The constructor for the value class is called +create+ and it expects a CIDR address string as a parameter. - # New values can be assigned to the value object using either another NetAddr::CIDR object, a string + # New values can be assigned to the value object using either another +NetAddr::CIDR+ object, a string # or an array. The <tt>:constructor</tt> and <tt>:converter</tt> options can be used to meet # these requirements: # @@ -173,7 +173,7 @@ module ActiveRecord # # == Finding records by a value object # - # Once a +composed_of+ relationship is specified for a model, records can be loaded from the database + # Once a #composed_of relationship is specified for a model, records can be loaded from the database # by specifying an instance of the value object in the conditions hash. The following example # finds all customers with +balance_amount+ equal to 20 and +balance_currency+ equal to "USD": # @@ -186,7 +186,7 @@ module ActiveRecord # Options are: # * <tt>:class_name</tt> - Specifies the class name of the association. Use it only if that name # can't be inferred from the part id. So <tt>composed_of :address</tt> will by default be linked - # to the Address class, but if the real class name is CompanyAddress, you'll have to specify it + # to the Address class, but if the real class name is +CompanyAddress+, you'll have to specify it # with this option. # * <tt>:mapping</tt> - Specifies the mapping of entity attributes to attributes of the value # object. Each mapping is represented as an array where the first item is the name of the diff --git a/activerecord/lib/active_record/associations.rb b/activerecord/lib/active_record/associations.rb index cf3e63b4a3..b806a2f832 100644 --- a/activerecord/lib/active_record/associations.rb +++ b/activerecord/lib/active_record/associations.rb @@ -149,7 +149,10 @@ module ActiveRecord class HasOneThroughNestedAssociationsAreReadonly < ThroughNestedAssociationsAreReadonly #:nodoc: end - class EagerLoadPolymorphicError < ActiveRecordError #:nodoc: + # This error is raised when trying to eager load a polymorphic association using a JOIN. + # Eager loading polymorphic associations is only possible with + # {ActiveRecord::Relation#preload}[rdoc-ref:QueryMethods#preload]. + class EagerLoadPolymorphicError < ActiveRecordError def initialize(reflection = nil) if reflection super("Cannot eagerly load the polymorphic association #{reflection.name.inspect}") @@ -298,7 +301,7 @@ module ActiveRecord # === A word of warning # # Don't create associations that have the same name as instance methods of - # <tt>ActiveRecord::Base</tt>. Since the association adds a method with that name to + # ActiveRecord::Base. Since the association adds a method with that name to # its model, it will override the inherited method and break things. # For instance, +attributes+ and +connection+ would be bad choices for association names. # @@ -360,7 +363,7 @@ module ActiveRecord # end # # If your model class is <tt>Project</tt>, the module is - # named <tt>Project::GeneratedAssociationMethods</tt>. The GeneratedAssociationMethods module is + # named <tt>Project::GeneratedAssociationMethods</tt>. The +GeneratedAssociationMethods+ 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. # @@ -368,12 +371,12 @@ module ActiveRecord # # Active Record associations can be used to describe one-to-one, one-to-many and many-to-many # relationships between models. Each model uses an association to describe its role in - # the relation. The +belongs_to+ association is always used in the model that has + # the relation. The #belongs_to association is always used in the model that has # the foreign key. # # === One-to-one # - # Use +has_one+ in the base, and +belongs_to+ in the associated model. + # Use #has_one in the base, and #belongs_to in the associated model. # # class Employee < ActiveRecord::Base # has_one :office @@ -384,7 +387,7 @@ module ActiveRecord # # === One-to-many # - # Use +has_many+ in the base, and +belongs_to+ in the associated model. + # Use #has_many in the base, and #belongs_to in the associated model. # # class Manager < ActiveRecord::Base # has_many :employees @@ -397,7 +400,7 @@ module ActiveRecord # # There are two ways to build a many-to-many relationship. # - # The first way uses a +has_many+ association with the <tt>:through</tt> option and a join model, so + # The first way uses a #has_many association with the <tt>:through</tt> option and a join model, so # there are two stages of associations. # # class Assignment < ActiveRecord::Base @@ -413,7 +416,7 @@ module ActiveRecord # has_many :programmers, through: :assignments # end # - # For the second way, use +has_and_belongs_to_many+ in both models. This requires a join table + # For the second way, use #has_and_belongs_to_many in both models. This requires a join table # that has no corresponding model or primary key. # # class Programmer < ActiveRecord::Base @@ -425,13 +428,13 @@ module ActiveRecord # # Choosing which way to build a many-to-many relationship is not always simple. # If you need to work with the relationship model as its own entity, - # use <tt>has_many :through</tt>. Use +has_and_belongs_to_many+ when working with legacy schemas or when + # use #has_many <tt>:through</tt>. Use #has_and_belongs_to_many when working with legacy schemas or when # you never work directly with the relationship itself. # - # == Is it a +belongs_to+ or +has_one+ association? + # == Is it a #belongs_to or #has_one association? # # Both express a 1-1 relationship. The difference is mostly where to place the foreign - # key, which goes on the table for the class declaring the +belongs_to+ relationship. + # key, which goes on the table for the class declaring the #belongs_to relationship. # # class User < ActiveRecord::Base # # I reference an account. @@ -464,35 +467,35 @@ module ActiveRecord # there is some special behavior you should be aware of, mostly involving the saving of # associated objects. # - # You can set the <tt>:autosave</tt> option on a <tt>has_one</tt>, <tt>belongs_to</tt>, - # <tt>has_many</tt>, or <tt>has_and_belongs_to_many</tt> association. Setting it + # You can set the <tt>:autosave</tt> option on a #has_one, #belongs_to, + # #has_many, or #has_and_belongs_to_many association. Setting it # to +true+ will _always_ save the members, whereas setting it to +false+ will # _never_ save the members. More details about <tt>:autosave</tt> option is available at # AutosaveAssociation. # # === One-to-one associations # - # * Assigning an object to a +has_one+ association automatically saves that object and + # * Assigning an object to a #has_one association automatically saves that object and # the object being replaced (if there is one), in order to update their foreign # keys - except if the parent object is unsaved (<tt>new_record? == true</tt>). # * If either of these saves fail (due to one of the objects being invalid), an - # <tt>ActiveRecord::RecordNotSaved</tt> exception is raised and the assignment is + # ActiveRecord::RecordNotSaved exception is raised and the assignment is # cancelled. - # * If you wish to assign an object to a +has_one+ association without saving it, - # use the <tt>build_association</tt> method (documented below). The object being + # * If you wish to assign an object to a #has_one association without saving it, + # use the <tt>#build_association</tt> method (documented below). The object being # replaced will still be saved to update its foreign key. - # * Assigning an object to a +belongs_to+ association does not save the object, since + # * Assigning an object to a #belongs_to association does not save the object, since # the foreign key field belongs on the parent. It does not save the parent either. # # === Collections # - # * Adding an object to a collection (+has_many+ or +has_and_belongs_to_many+) automatically + # * Adding an object to a collection (#has_many or #has_and_belongs_to_many) automatically # saves that object, except if the parent object (the owner of the collection) is not yet # stored in the database. # * If saving any of the objects being added to a collection (via <tt>push</tt> or similar) # fails, then <tt>push</tt> returns +false+. # * If saving fails while replacing the collection (via <tt>association=</tt>), an - # <tt>ActiveRecord::RecordNotSaved</tt> exception is raised and the assignment is + # ActiveRecord::RecordNotSaved exception is raised and the assignment is # cancelled. # * You can add an object to a collection without automatically saving it by using the # <tt>collection.build</tt> method (documented below). @@ -501,14 +504,14 @@ module ActiveRecord # # == Customizing the query # - # \Associations are built from <tt>Relation</tt>s, and you can use the <tt>Relation</tt> syntax + # \Associations are built from <tt>Relation</tt>s, and you can use the Relation syntax # to customize them. For example, to add a condition: # # class Blog < ActiveRecord::Base - # has_many :published_posts, -> { where published: true }, class_name: 'Post' + # 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. + # Inside the <tt>-> { ... }</tt> block you can use all of the usual Relation methods. # # === Accessing the owner object # @@ -517,7 +520,7 @@ module ActiveRecord # 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' + # has_many :birthday_events, ->(user) { where(starts_on: user.birthday) }, class_name: 'Event' # end # # Note: Joining, eager loading and preloading of these associations is not fully possible. @@ -596,8 +599,8 @@ module ActiveRecord # # * <tt>record.association(:items).owner</tt> - Returns the object the association is part of. # * <tt>record.association(:items).reflection</tt> - Returns the reflection object that describes the association. - # * <tt>record.association(:items).target</tt> - Returns the associated object for +belongs_to+ and +has_one+, or - # the collection of associated objects for +has_many+ and +has_and_belongs_to_many+. + # * <tt>record.association(:items).target</tt> - Returns the associated object for #belongs_to and #has_one, or + # the collection of associated objects for #has_many and #has_and_belongs_to_many. # # However, inside the actual extension code, you will not have access to the <tt>record</tt> as # above. In this case, you can access <tt>proxy_association</tt>. For example, @@ -609,7 +612,7 @@ module ActiveRecord # # Has Many associations can be configured with the <tt>:through</tt> option to use an # explicit join model to retrieve the data. This operates similarly to a - # +has_and_belongs_to_many+ association. The advantage is that you're able to add validations, + # #has_and_belongs_to_many association. The advantage is that you're able to add validations, # callbacks, and extra attributes on the join model. Consider the following schema: # # class Author < ActiveRecord::Base @@ -626,7 +629,7 @@ module ActiveRecord # @author.authorships.collect { |a| a.book } # selects all books that the author's authorships belong to # @author.books # selects all books by using the Authorship join model # - # You can also go through a +has_many+ association on the join model: + # You can also go through a #has_many association on the join model: # # class Firm < ActiveRecord::Base # has_many :clients @@ -646,7 +649,7 @@ module ActiveRecord # @firm.clients.flat_map { |c| c.invoices } # select all invoices for all clients of the firm # @firm.invoices # selects all invoices by going through the Client join model # - # Similarly you can go through a +has_one+ association on the join model: + # Similarly you can go through a #has_one association on the join model: # # class Group < ActiveRecord::Base # has_many :users @@ -666,7 +669,7 @@ module ActiveRecord # @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 + # An important caveat with going through #has_one or #has_many associations on the # join model is that these associations are *read-only*. For example, the following # would not work following the previous example: # @@ -675,9 +678,9 @@ module ActiveRecord # # == Setting Inverses # - # If you are using a +belongs_to+ on the join model, it is a good idea to set the - # <tt>:inverse_of</tt> option on the +belongs_to+, which will mean that the following example - # works correctly (where <tt>tags</tt> is a +has_many+ <tt>:through</tt> association): + # If you are using a #belongs_to on the join model, it is a good idea to set the + # <tt>:inverse_of</tt> option on the #belongs_to, which will mean that the following example + # works correctly (where <tt>tags</tt> is a #has_many <tt>:through</tt> association): # # @post = Post.first # @tag = @post.tags.build name: "ruby" @@ -693,8 +696,8 @@ module ActiveRecord # # If you do not set the <tt>:inverse_of</tt> record, the association will # do its best to match itself up with the correct inverse. Automatic - # inverse detection only works on <tt>has_many</tt>, <tt>has_one</tt>, and - # <tt>belongs_to</tt> associations. + # inverse detection only works on #has_many, #has_one, and + # #belongs_to associations. # # Extra options on the associations, as defined in the # <tt>AssociationReflection::INVALID_AUTOMATIC_INVERSE_OPTIONS</tt> constant, will @@ -757,7 +760,7 @@ module ActiveRecord # == 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 + # can be associated with. Rather, they specify an interface that a #has_many association # must adhere to. # # class Asset < ActiveRecord::Base @@ -841,7 +844,7 @@ module ActiveRecord # # Post.includes(:author).each do |post| # - # This references the name of the +belongs_to+ association that also used the <tt>:author</tt> + # This references the name of the #belongs_to association that also used the <tt>:author</tt> # symbol. After loading the posts, find will collect the +author_id+ from each one and load # all the referenced authors with one query. Doing so will cut down the number of queries # from 201 to 102. @@ -852,7 +855,7 @@ module ActiveRecord # # This will load all comments with a single query. This reduces the total number of queries # to 3. In general, the number of queries will be 1 plus the number of associations - # named (except if some of the associations are polymorphic +belongs_to+ - see below). + # named (except if some of the associations are polymorphic #belongs_to - see below). # # To include a deep hierarchy of associations, use a hash: # @@ -892,7 +895,7 @@ module ActiveRecord # In this case it is usually more natural to include an association which has conditions defined on it: # # class Post < ActiveRecord::Base - # has_many :approved_comments, -> { where approved: true }, class_name: 'Comment' + # has_many :approved_comments, -> { where(approved: true) }, class_name: 'Comment' # end # # Post.includes(:approved_comments) @@ -924,7 +927,7 @@ module ActiveRecord # For example if all the addressables are either of class Person or Company then a total # of 3 queries will be executed. The list of addressable types to load is determined on # the back of the addresses loaded. This is not supported if Active Record has to fallback - # to the previous implementation of eager loading and will raise <tt>ActiveRecord::EagerLoadPolymorphicError</tt>. + # to the previous implementation of eager loading and will raise ActiveRecord::EagerLoadPolymorphicError. # The reason is that the parent model's type is a column value so its corresponding table # name cannot be put in the +FROM+/+JOIN+ clauses of that query. # @@ -966,7 +969,7 @@ module ActiveRecord # INNER JOIN categories_posts posts_categories_join INNER JOIN posts posts_categories # INNER JOIN categories_posts categories_posts_join INNER JOIN categories categories_posts_2 # - # If you wish to specify your own custom joins using <tt>joins</tt> method, those table + # If you wish to specify your own custom joins using ActiveRecord::QueryMethods#joins method, those table # names will take precedence over the eager associations: # # Post.joins(:comments).joins("inner join comments ...") @@ -1059,7 +1062,7 @@ module ActiveRecord # # * does not work with <tt>:through</tt> associations. # * does not work with <tt>:polymorphic</tt> associations. - # * for +belongs_to+ associations +has_many+ inverse associations are ignored. + # * for #belongs_to associations #has_many inverse associations are ignored. # # For more information, see the documentation for the +:inverse_of+ option. # @@ -1067,7 +1070,7 @@ module ActiveRecord # # === Dependent associations # - # +has_many+, +has_one+ and +belongs_to+ associations support the <tt>:dependent</tt> option. + # #has_many, #has_one and #belongs_to associations support the <tt>:dependent</tt> option. # This allows you to specify that associated records should be deleted when the owner is # deleted. # @@ -1088,22 +1091,22 @@ module ActiveRecord # callbacks declared either before or after the <tt>:dependent</tt> option # can affect what it does. # - # Note that <tt>:dependent</tt> option is ignored for +has_one+ <tt>:through</tt> associations. + # Note that <tt>:dependent</tt> option is ignored for #has_one <tt>:through</tt> associations. # # === Delete or destroy? # - # +has_many+ and +has_and_belongs_to_many+ associations have the methods <tt>destroy</tt>, + # #has_many and #has_and_belongs_to_many associations have the methods <tt>destroy</tt>, # <tt>delete</tt>, <tt>destroy_all</tt> and <tt>delete_all</tt>. # - # For +has_and_belongs_to_many+, <tt>delete</tt> and <tt>destroy</tt> are the same: they + # For #has_and_belongs_to_many, <tt>delete</tt> and <tt>destroy</tt> are the same: they # cause the records in the join table to be removed. # - # For +has_many+, <tt>destroy</tt> and <tt>destroy_all</tt> will always call the <tt>destroy</tt> method of the + # For #has_many, <tt>destroy</tt> and <tt>destroy_all</tt> will always call the <tt>destroy</tt> method of the # record(s) being removed so that callbacks are run. However <tt>delete</tt> and <tt>delete_all</tt> will either # do the deletion according to the strategy specified by the <tt>:dependent</tt> option, or # if no <tt>:dependent</tt> option is given, then it will follow the default strategy. # The default strategy is to do nothing (leave the foreign keys with the parent ids set), except for - # +has_many+ <tt>:through</tt>, where the default strategy is <tt>delete_all</tt> (delete + # #has_many <tt>:through</tt>, where the default strategy is <tt>delete_all</tt> (delete # the join records, without running their callbacks). # # There is also a <tt>clear</tt> method which is the same as <tt>delete_all</tt>, except that @@ -1111,13 +1114,13 @@ module ActiveRecord # # === What gets deleted? # - # There is a potential pitfall here: +has_and_belongs_to_many+ and +has_many+ <tt>:through</tt> + # There is a potential pitfall here: #has_and_belongs_to_many and #has_many <tt>:through</tt> # associations have records in join tables, as well as the associated records. So when we # call one of these deletion methods, what exactly should be deleted? # # The answer is that it is assumed that deletion on an association is about removing the # <i>link</i> between the owner and the associated object(s), rather than necessarily the - # associated objects themselves. So with +has_and_belongs_to_many+ and +has_many+ + # associated objects themselves. So with #has_and_belongs_to_many and #has_many # <tt>:through</tt>, the join records will be deleted, but the associated records won't. # # This makes sense if you think about it: if you were to call <tt>post.tags.delete(Tag.find_by(name: 'food'))</tt> @@ -1128,20 +1131,20 @@ module ActiveRecord # a person has many projects, and each project has many tasks. If we deleted one of a person's # tasks, we would probably not want the project to be deleted. In this scenario, the delete method # won't actually work: it can only be used if the association on the join model is a - # +belongs_to+. In other situations you are expected to perform operations directly on + # #belongs_to. In other situations you are expected to perform operations directly on # either the associated records or the <tt>:through</tt> association. # - # With a regular +has_many+ there is no distinction between the "associated records" + # With a regular #has_many there is no distinction between the "associated records" # and the "link", so there is only one choice for what gets deleted. # - # With +has_and_belongs_to_many+ and +has_many+ <tt>:through</tt>, if you want to delete the + # With #has_and_belongs_to_many and #has_many <tt>:through</tt>, if you want to delete the # associated records themselves, you can always do something along the lines of # <tt>person.tasks.each(&:destroy)</tt>. # - # == Type safety with <tt>ActiveRecord::AssociationTypeMismatch</tt> + # == Type safety with ActiveRecord::AssociationTypeMismatch # # If you attempt to assign an object to an association that doesn't match the inferred - # or specified <tt>:class_name</tt>, you'll get an <tt>ActiveRecord::AssociationTypeMismatch</tt>. + # or specified <tt>:class_name</tt>, you'll get an ActiveRecord::AssociationTypeMismatch. # # == Options # @@ -1195,10 +1198,10 @@ module ActiveRecord # [collection.size] # Returns the number of associated objects. # [collection.find(...)] - # Finds an associated object according to the same rules as <tt>ActiveRecord::Base.find</tt>. + # Finds an associated object according to the same rules as ActiveRecord::FinderMethods#find. # [collection.exists?(...)] # Checks whether an associated object with the given conditions exists. - # Uses the same rules as <tt>ActiveRecord::Base.exists?</tt>. + # Uses the same rules as ActiveRecord::FinderMethods#exists?. # [collection.build(attributes = {}, ...)] # Returns one or more new objects of the collection type that have been instantiated # with +attributes+ and linked to this object through a foreign key, but have not yet @@ -1209,7 +1212,7 @@ module ActiveRecord # been saved (if it passed the validation). *Note*: This only works if the base model # already exists in the DB, not if it is a new (unsaved) record! # [collection.create!(attributes = {})] - # Does the same as <tt>collection.create</tt>, but raises <tt>ActiveRecord::RecordInvalid</tt> + # Does the same as <tt>collection.create</tt>, but raises ActiveRecord::RecordInvalid # if the record is invalid. # # === Example @@ -1261,11 +1264,11 @@ module ActiveRecord # [:class_name] # Specify the class name of the association. Use it only if that name can't be inferred # from the association name. So <tt>has_many :products</tt> will by default be linked - # to the Product class, but if the real class name is SpecialProduct, you'll have to + # to the +Product+ class, but if the real class name is +SpecialProduct+, you'll have to # specify it with this option. # [:foreign_key] # Specify the foreign key used for the association. By default this is guessed to be the name - # of this class in lower-case and "_id" suffixed. So a Person class that makes a +has_many+ + # of this class in lower-case and "_id" suffixed. So a Person class that makes a #has_many # association will use "person_id" as the default <tt>:foreign_key</tt>. # [:foreign_type] # Specify the column used to store the associated object's type, if this is a polymorphic @@ -1289,20 +1292,20 @@ module ActiveRecord # * <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 + # a #belongs_to, and the records which get deleted are the join records, rather than # the associated records. # [:counter_cache] # This option can be used to configure a custom named <tt>:counter_cache.</tt> You only need this option, - # when you customized the name of your <tt>:counter_cache</tt> on the <tt>belongs_to</tt> association. + # when you customized the name of your <tt>:counter_cache</tt> on the #belongs_to association. # [:as] - # Specifies a polymorphic interface (See <tt>belongs_to</tt>). + # Specifies a polymorphic interface (See #belongs_to). # [:through] # Specifies an association through which to perform the query. This can be any other type # of association, including other <tt>:through</tt> associations. Options for <tt>:class_name</tt>, # <tt>:primary_key</tt> and <tt>:foreign_key</tt> are ignored, as the association uses the # source reflection. # - # If the association on the join model is a +belongs_to+, the collection can be modified + # If the association on the join model is a #belongs_to, the collection can be modified # and the records on the <tt>:through</tt> model will be automatically created and removed # as appropriate. Otherwise, the collection is read-only, so you should manipulate the # <tt>:through</tt> association directly. @@ -1313,13 +1316,13 @@ module ActiveRecord # the appropriate join model records when they are saved. (See the 'Association Join Models' # section above.) # [:source] - # Specifies the source association name used by <tt>has_many :through</tt> queries. + # Specifies the source association name used by #has_many <tt>:through</tt> queries. # Only use it if the name cannot be inferred from the association. # <tt>has_many :subscribers, through: :subscriptions</tt> will look for either <tt>:subscribers</tt> or # <tt>:subscriber</tt> on Subscription, unless a <tt>:source</tt> is given. # [:source_type] - # Specifies type of the source association used by <tt>has_many :through</tt> queries where the source - # association is a polymorphic +belongs_to+. + # Specifies type of the source association used by #has_many <tt>:through</tt> queries where the source + # association is a polymorphic #belongs_to. # [:validate] # If +false+, don't validate the associated objects when saving the parent object. true by default. # [:autosave] @@ -1329,10 +1332,11 @@ module ActiveRecord # +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>. + # Note that NestedAttributes::ClassMethods#accepts_nested_attributes_for 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 + # Specifies the name of the #belongs_to association on the associated object + # that is the inverse of this #has_many association. Does not work in combination # with <tt>:through</tt> or <tt>:as</tt> options. # See ActiveRecord::Associations::ClassMethods's overview on Bi-directional associations for more detail. # [:extend] @@ -1341,10 +1345,10 @@ module ActiveRecord # association objects. # # Option examples: - # has_many :comments, -> { order "posted_on" } - # has_many :comments, -> { includes :author } + # has_many :comments, -> { order("posted_on") } + # has_many :comments, -> { includes(:author) } # has_many :people, -> { where(deleted: false).order("name") }, class_name: "Person" - # has_many :tracks, -> { order "position" }, dependent: :destroy + # has_many :tracks, -> { order("position") }, dependent: :destroy # has_many :comments, dependent: :nullify # has_many :tags, as: :taggable # has_many :reports, -> { readonly } @@ -1356,8 +1360,8 @@ module ActiveRecord # Specifies a one-to-one association with another class. This method should only be used # if the other class contains the foreign key. If the current class contains the foreign key, - # then you should use +belongs_to+ instead. See also ActiveRecord::Associations::ClassMethods's overview - # on when to use +has_one+ and when to use +belongs_to+. + # then you should use #belongs_to instead. See also ActiveRecord::Associations::ClassMethods's overview + # on when to use #has_one and when to use #belongs_to. # # The following methods for retrieval and query of a single associated object will be added: # @@ -1379,7 +1383,7 @@ module ActiveRecord # with +attributes+, linked to this object through a foreign key, and that # has already been saved (if it passed the validation). # [create_association!(attributes = {})] - # Does the same as <tt>create_association</tt>, but raises <tt>ActiveRecord::RecordInvalid</tt> + # Does the same as <tt>create_association</tt>, but raises ActiveRecord::RecordInvalid # if the record is invalid. # # === Example @@ -1424,7 +1428,7 @@ module ActiveRecord # Note that <tt>:dependent</tt> option is ignored when using <tt>:through</tt> option. # [:foreign_key] # Specify the foreign key used for the association. By default this is guessed to be the name - # of this class in lower-case and "_id" suffixed. So a Person class that makes a +has_one+ association + # of this class in lower-case and "_id" suffixed. So a Person class that makes a #has_one association # will use "person_id" as the default <tt>:foreign_key</tt>. # [:foreign_type] # Specify the column used to store the associated object's type, if this is a polymorphic @@ -1435,20 +1439,20 @@ module ActiveRecord # [:primary_key] # Specify the method that returns the primary key used for the association. By default this is +id+. # [:as] - # Specifies a polymorphic interface (See <tt>belongs_to</tt>). + # Specifies a polymorphic interface (See #belongs_to). # [: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 - # source reflection. You can only use a <tt>:through</tt> query through a <tt>has_one</tt> - # or <tt>belongs_to</tt> association on the join model. + # source reflection. You can only use a <tt>:through</tt> query through a #has_one + # or #belongs_to association on the join model. # [:source] - # Specifies the source association name used by <tt>has_one :through</tt> queries. + # Specifies the source association name used by #has_one <tt>:through</tt> queries. # Only use it if the name cannot be inferred from the association. # <tt>has_one :favorite, through: :favorites</tt> will look for a # <tt>:favorite</tt> on Favorite, unless a <tt>:source</tt> is given. # [:source_type] - # Specifies type of the source association used by <tt>has_one :through</tt> queries where the source - # association is a polymorphic +belongs_to+. + # Specifies type of the source association used by #has_one <tt>:through</tt> queries where the source + # association is a polymorphic #belongs_to. # [:validate] # If +false+, don't validate the associated object when saving the parent object. +false+ by default. # [:autosave] @@ -1456,10 +1460,11 @@ module ActiveRecord # 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>. + # Note that NestedAttributes::ClassMethods#accepts_nested_attributes_for 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 + # Specifies the name of the #belongs_to association on the associated object + # that is the inverse of this #has_one association. Does not work in combination # with <tt>:through</tt> or <tt>:as</tt> options. # See ActiveRecord::Associations::ClassMethods's overview on Bi-directional associations for more detail. # [:required] @@ -1471,12 +1476,12 @@ 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, -> { order 'posted_on' }, class_name: "Comment" - # has_one :project_manager, -> { where role: 'project_manager' }, class_name: "Person" + # 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 } # has_one :club, through: :membership - # has_one :primary_address, -> { where primary: true }, through: :addressables, source: :addressable + # has_one :primary_address, -> { where(primary: true) }, through: :addressables, source: :addressable # has_one :credit_card, required: true def has_one(name, scope = nil, options = {}) reflection = Builder::HasOne.build(self, name, scope, options) @@ -1485,8 +1490,8 @@ module ActiveRecord # Specifies a one-to-one association with another class. This method should only be used # if this class contains the foreign key. If the other class contains the foreign key, - # then you should use +has_one+ instead. See also ActiveRecord::Associations::ClassMethods's overview - # on when to use +has_one+ and when to use +belongs_to+. + # then you should use #has_one instead. See also ActiveRecord::Associations::ClassMethods's overview + # on when to use #has_one and when to use #belongs_to. # # Methods will be added for retrieval and query for a single associated object, for which # this object holds an id: @@ -1506,7 +1511,7 @@ module ActiveRecord # with +attributes+, linked to this object through a foreign key, and that # has already been saved (if it passed the validation). # [create_association!(attributes = {})] - # Does the same as <tt>create_association</tt>, but raises <tt>ActiveRecord::RecordInvalid</tt> + # Does the same as <tt>create_association</tt>, but raises ActiveRecord::RecordInvalid # if the record is invalid. # # === Example @@ -1553,12 +1558,12 @@ module ActiveRecord # [:dependent] # If set to <tt>:destroy</tt>, the associated object is destroyed when this object is. If set to # <tt>:delete</tt>, the associated object is deleted *without* calling its destroy method. - # This option should not be specified when <tt>belongs_to</tt> is used in conjunction with - # a <tt>has_many</tt> relationship on another class because of the potential to leave + # This option should not be specified when #belongs_to is used in conjunction with + # a #has_many relationship on another class because of the potential to leave # orphaned records behind. # [:counter_cache] - # Caches the number of belonging objects on the associate class through the use of +increment_counter+ - # and +decrement_counter+. The counter cache is incremented when an object of this + # Caches the number of belonging objects on the associate class through the use of CounterCache::ClassMethods#increment_counter + # and CounterCache::ClassMethods#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) - that is the migration for @@ -1580,14 +1585,15 @@ module ActiveRecord # 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>. + # Note that NestedAttributes::ClassMethods#accepts_nested_attributes_for + # sets <tt>:autosave</tt> to <tt>true</tt>. # [:touch] # If true, the associated object will be touched (the updated_at/on attributes set to current time) # when this record is either saved or destroyed. If you specify a symbol, that attribute # will be updated with the current time in addition to the updated_at/on attribute. # [:inverse_of] - # Specifies the name of the <tt>has_one</tt> or <tt>has_many</tt> association on the associated - # object that is the inverse of this <tt>belongs_to</tt> association. Does not work in + # Specifies the name of the #has_one or #has_many association on the associated + # object that is the inverse of this #belongs_to association. Does not work in # combination with the <tt>:polymorphic</tt> options. # See ActiveRecord::Associations::ClassMethods's overview on Bi-directional associations for more detail. # [:optional] @@ -1677,10 +1683,10 @@ module ActiveRecord # [collection.find(id)] # Finds an associated object responding to the +id+ and that # meets the condition that it has to be associated with this object. - # Uses the same rules as <tt>ActiveRecord::Base.find</tt>. + # Uses the same rules as ActiveRecord::FinderMethods#find. # [collection.exists?(...)] # Checks whether an associated object with the given conditions exists. - # Uses the same rules as <tt>ActiveRecord::Base.exists?</tt>. + # Uses the same rules as ActiveRecord::FinderMethods#exists?. # [collection.build(attributes = {})] # Returns a new object of the collection type that has been instantiated # with +attributes+ and linked to this object through the join table, but has not yet been saved. @@ -1715,7 +1721,7 @@ module ActiveRecord # query when you access the associated collection. # # Scope examples: - # has_and_belongs_to_many :projects, -> { includes :milestones, :manager } + # has_and_belongs_to_many :projects, -> { includes(:milestones, :manager) } # has_and_belongs_to_many :categories, ->(category) { # where("default_category = ?", category.name) # } @@ -1744,19 +1750,17 @@ module ActiveRecord # [:join_table] # Specify the name of the join table if the default based on lexical order isn't what you want. # <b>WARNING:</b> If you're overwriting the table name of either class, the +table_name+ method - # MUST be declared underneath any +has_and_belongs_to_many+ declaration in order to work. + # MUST be declared underneath any #has_and_belongs_to_many declaration in order to work. # [:foreign_key] # Specify the foreign key used for the association. By default this is guessed to be the name # of this class in lower-case and "_id" suffixed. So a Person class that makes - # a +has_and_belongs_to_many+ association to Project will use "person_id" as the + # a #has_and_belongs_to_many association to Project will use "person_id" as the # default <tt>:foreign_key</tt>. # [:association_foreign_key] # Specify the foreign key used for the association on the receiving side of the association. # 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, + # 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>. - # [: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] @@ -1765,11 +1769,12 @@ 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>. + # Note that NestedAttributes::ClassMethods#accepts_nested_attributes_for sets + # <tt>:autosave</tt> to <tt>true</tt>. # # Option examples: # has_and_belongs_to_many :projects - # has_and_belongs_to_many :projects, -> { includes :milestones, :manager } + # 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 } diff --git a/activerecord/lib/active_record/associations/association_scope.rb b/activerecord/lib/active_record/associations/association_scope.rb index a140dc239c..48437a1c9e 100644 --- a/activerecord/lib/active_record/associations/association_scope.rb +++ b/activerecord/lib/active_record/associations/association_scope.rb @@ -147,9 +147,9 @@ module ActiveRecord scope.includes! item.includes_values end + scope.unscope!(*item.unscope_values) scope.where_clause += item.where_clause scope.order_values |= item.order_values - scope.unscope!(*item.unscope_values) end reflection = reflection.next diff --git a/activerecord/lib/active_record/associations/belongs_to_association.rb b/activerecord/lib/active_record/associations/belongs_to_association.rb index 260a0c6a2d..41698c5360 100644 --- a/activerecord/lib/active_record/associations/belongs_to_association.rb +++ b/activerecord/lib/active_record/associations/belongs_to_association.rb @@ -10,7 +10,7 @@ module ActiveRecord def replace(record) if record raise_on_type_mismatch!(record) - update_counters(record) + update_counters_on_replace(record) replace_keys(record) set_inverse_instance(record) @updated = true @@ -32,45 +32,37 @@ module ActiveRecord end def decrement_counters # :nodoc: - with_cache_name { |name| decrement_counter name } + update_counters(-1) end def increment_counters # :nodoc: - with_cache_name { |name| increment_counter name } + update_counters(1) end private - def find_target? - !loaded? && foreign_key_present? && klass - end - - def with_cache_name - counter_cache_name = reflection.counter_cache_column - return unless counter_cache_name && owner.persisted? - yield counter_cache_name + def update_counters(by) + if require_counter_update? && foreign_key_present? + if target && !stale_target? + target.increment!(reflection.counter_cache_column, by) + else + klass.update_counters(target_id, reflection.counter_cache_column => by) + end + end end - def update_counters(record) - with_cache_name do |name| - return unless different_target? record - record.class.increment_counter(name, record.id) - decrement_counter name - end + def find_target? + !loaded? && foreign_key_present? && klass end - def decrement_counter(counter_cache_name) - if foreign_key_present? - klass.decrement_counter(counter_cache_name, target_id) - end + def require_counter_update? + reflection.counter_cache_column && owner.persisted? end - def increment_counter(counter_cache_name) - if foreign_key_present? - klass.increment_counter(counter_cache_name, target_id) - if target && !stale_target? - target.increment(counter_cache_name) - end + def update_counters_on_replace(record) + if require_counter_update? && different_target?(record) + record.increment!(reflection.counter_cache_column) + decrement_counters end end diff --git a/activerecord/lib/active_record/associations/builder/association.rb b/activerecord/lib/active_record/associations/builder/association.rb index ba1b1814d1..d0534056d9 100644 --- a/activerecord/lib/active_record/associations/builder/association.rb +++ b/activerecord/lib/active_record/associations/builder/association.rb @@ -9,7 +9,7 @@ # - CollectionAssociation # - HasManyAssociation -module ActiveRecord::Associations::Builder +module ActiveRecord::Associations::Builder # :nodoc: class Association #:nodoc: class << self attr_accessor :extensions diff --git a/activerecord/lib/active_record/associations/builder/belongs_to.rb b/activerecord/lib/active_record/associations/builder/belongs_to.rb index 6e4a53f7fb..dae468ba54 100644 --- a/activerecord/lib/active_record/associations/builder/belongs_to.rb +++ b/activerecord/lib/active_record/associations/builder/belongs_to.rb @@ -1,4 +1,4 @@ -module ActiveRecord::Associations::Builder +module ActiveRecord::Associations::Builder # :nodoc: class BelongsTo < SingularAssociation #:nodoc: def self.macro :belongs_to diff --git a/activerecord/lib/active_record/associations/builder/collection_association.rb b/activerecord/lib/active_record/associations/builder/collection_association.rb index 2ff67f904d..56a8dc4e18 100644 --- a/activerecord/lib/active_record/associations/builder/collection_association.rb +++ b/activerecord/lib/active_record/associations/builder/collection_association.rb @@ -2,7 +2,7 @@ require 'active_record/associations' -module ActiveRecord::Associations::Builder +module ActiveRecord::Associations::Builder # :nodoc: class CollectionAssociation < Association #:nodoc: CALLBACKS = [:before_add, :after_add, :before_remove, :after_remove] 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 b18d99d54e..b888148841 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,9 +1,9 @@ -module ActiveRecord::Associations::Builder +module ActiveRecord::Associations::Builder # :nodoc: class HasAndBelongsToMany # :nodoc: - class JoinTableResolver + class JoinTableResolver # :nodoc: KnownTable = Struct.new :join_table - class KnownClass + class KnownClass # :nodoc: def initialize(lhs_class, rhs_class_name) @lhs_class = lhs_class @rhs_class_name = rhs_class_name @@ -62,13 +62,13 @@ module ActiveRecord::Associations::Builder end def self.add_left_association(name, options) - belongs_to name, options + belongs_to name, required: false, **options self.left_reflection = _reflect_on_association(name) end def self.add_right_association(name, options) rhs_name = name.to_s.singularize.to_sym - belongs_to rhs_name, options + belongs_to rhs_name, required: false, **options self.right_reflection = _reflect_on_association(rhs_name) end diff --git a/activerecord/lib/active_record/associations/builder/has_many.rb b/activerecord/lib/active_record/associations/builder/has_many.rb index 1c1b47bd56..7864d4c536 100644 --- a/activerecord/lib/active_record/associations/builder/has_many.rb +++ b/activerecord/lib/active_record/associations/builder/has_many.rb @@ -1,11 +1,11 @@ -module ActiveRecord::Associations::Builder +module ActiveRecord::Associations::Builder # :nodoc: class HasMany < CollectionAssociation #:nodoc: def self.macro :has_many end def self.valid_options(options) - super + [:primary_key, :dependent, :as, :through, :source, :source_type, :inverse_of, :counter_cache, :join_table, :foreign_type] + super + [:primary_key, :dependent, :as, :through, :source, :source_type, :inverse_of, :counter_cache, :join_table, :foreign_type, :index_errors] end def self.valid_dependent_options diff --git a/activerecord/lib/active_record/associations/builder/has_one.rb b/activerecord/lib/active_record/associations/builder/has_one.rb index a272d3c781..9d64ae877b 100644 --- a/activerecord/lib/active_record/associations/builder/has_one.rb +++ b/activerecord/lib/active_record/associations/builder/has_one.rb @@ -1,4 +1,4 @@ -module ActiveRecord::Associations::Builder +module ActiveRecord::Associations::Builder # :nodoc: class HasOne < SingularAssociation #:nodoc: def self.macro :has_one diff --git a/activerecord/lib/active_record/associations/builder/singular_association.rb b/activerecord/lib/active_record/associations/builder/singular_association.rb index 42542f188e..58a9c8ff24 100644 --- a/activerecord/lib/active_record/associations/builder/singular_association.rb +++ b/activerecord/lib/active_record/associations/builder/singular_association.rb @@ -1,6 +1,6 @@ # This class is inherited by the has_one and belongs_to association classes -module ActiveRecord::Associations::Builder +module ActiveRecord::Associations::Builder # :nodoc: class SingularAssociation < Association #:nodoc: def self.valid_options(options) super + [:dependent, :primary_key, :inverse_of, :required] diff --git a/activerecord/lib/active_record/associations/collection_association.rb b/activerecord/lib/active_record/associations/collection_association.rb index 256df3ca11..f32dddb8f0 100644 --- a/activerecord/lib/active_record/associations/collection_association.rb +++ b/activerecord/lib/active_record/associations/collection_association.rb @@ -1,5 +1,3 @@ -require "active_support/deprecation" - module ActiveRecord module Associations # = Active Record Association Collection diff --git a/activerecord/lib/active_record/associations/collection_proxy.rb b/activerecord/lib/active_record/associations/collection_proxy.rb index 9994b72158..fe693cfbb6 100644 --- a/activerecord/lib/active_record/associations/collection_proxy.rb +++ b/activerecord/lib/active_record/associations/collection_proxy.rb @@ -112,7 +112,7 @@ module ActiveRecord end # Finds an object in the collection responding to the +id+. Uses the same - # rules as <tt>ActiveRecord::Base.find</tt>. Returns <tt>ActiveRecord::RecordNotFound</tt> + # rules as ActiveRecord::Base.find. Returns ActiveRecord::RecordNotFound # error if the object cannot be found. # # class Person < ActiveRecord::Base @@ -171,27 +171,27 @@ module ActiveRecord @association.first(*args) end - # Same as +first+ except returns only the second record. + # Same as #first except returns only the second record. def second(*args) @association.second(*args) end - # Same as +first+ except returns only the third record. + # Same as #first except returns only the third record. def third(*args) @association.third(*args) end - # Same as +first+ except returns only the fourth record. + # Same as #first except returns only the fourth record. def fourth(*args) @association.fourth(*args) end - # Same as +first+ except returns only the fifth record. + # Same as #first except returns only the fifth record. def fifth(*args) @association.fifth(*args) end - # Same as +first+ except returns only the forty second record. + # Same as #first except returns only the forty second record. # Also known as accessing "the reddit". def forty_two(*args) @association.forty_two(*args) @@ -315,7 +315,7 @@ module ActiveRecord @association.create(attributes, &block) end - # Like +create+, except that if the record is invalid, raises an exception. + # Like #create, except that if the record is invalid, raises an exception. # # class Person # has_many :pets @@ -332,8 +332,8 @@ module ActiveRecord 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+ + # 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 @@ -389,7 +389,7 @@ module ActiveRecord # specified by the +:dependent+ option. If no +:dependent+ option is given, # then it will follow the default strategy. # - # For +has_many :through+ associations, the default deletion strategy is + # For <tt>has_many :through</tt> associations, the default deletion strategy is # +:delete_all+. # # For +has_many+ associations, the default deletion strategy is +:nullify+. @@ -424,7 +424,7 @@ module ActiveRecord # # #<Pet id: 3, name: "Choo-Choo", person_id: nil> # # ] # - # Both +has_many+ and +has_many :through+ dependencies default to the + # Both +has_many+ and <tt>has_many :through</tt> dependencies default to the # +:delete_all+ strategy if the +:dependent+ option is set to +:destroy+. # Records are not instantiated and callbacks will not be fired. # @@ -500,7 +500,7 @@ module ActiveRecord # then it will follow the default strategy. Returns an array with the # deleted records. # - # For +has_many :through+ associations, the default deletion strategy is + # For <tt>has_many :through</tt> associations, the default deletion strategy is # +:delete_all+. # # For +has_many+ associations, the default deletion strategy is +:nullify+. diff --git a/activerecord/lib/active_record/associations/foreign_association.rb b/activerecord/lib/active_record/associations/foreign_association.rb index fe48ecec29..3ceec0ee46 100644 --- a/activerecord/lib/active_record/associations/foreign_association.rb +++ b/activerecord/lib/active_record/associations/foreign_association.rb @@ -1,5 +1,5 @@ module ActiveRecord::Associations - module ForeignAssociation + module ForeignAssociation # :nodoc: def foreign_key_present? if reflection.klass.primary_key owner.attribute_present?(reflection.active_record_primary_key) diff --git a/activerecord/lib/active_record/associations/has_many_association.rb b/activerecord/lib/active_record/associations/has_many_association.rb index 7da20d8eea..a9f6aaafef 100644 --- a/activerecord/lib/active_record/associations/has_many_association.rb +++ b/activerecord/lib/active_record/associations/has_many_association.rb @@ -88,21 +88,15 @@ module ActiveRecord end def update_counter(difference, reflection = reflection()) - update_counter_in_database(difference, reflection) - update_counter_in_memory(difference, reflection) - end - - def update_counter_in_database(difference, reflection = reflection()) if reflection.has_cached_counter? - owner.class.update_counters(owner.id, reflection.counter_cache_column => difference) + owner.increment!(reflection.counter_cache_column, difference) end end def update_counter_in_memory(difference, reflection = reflection()) if reflection.counter_must_be_updated_by_has_many? counter = reflection.counter_cache_column - owner[counter] ||= 0 - owner[counter] += difference + owner.increment(counter, difference) owner.send(:clear_attribute_change, counter) # eww end end diff --git a/activerecord/lib/active_record/associations/join_dependency.rb b/activerecord/lib/active_record/associations/join_dependency.rb index 81eb5136a1..9f183c3e7e 100644 --- a/activerecord/lib/active_record/associations/join_dependency.rb +++ b/activerecord/lib/active_record/associations/join_dependency.rb @@ -103,9 +103,14 @@ module ActiveRecord join_root.drop(1).map!(&:reflection) end - def join_constraints(outer_joins) + def join_constraints(outer_joins, join_type) joins = join_root.children.flat_map { |child| - make_inner_joins join_root, child + + if join_type == Arel::Nodes::OuterJoin + make_left_outer_joins join_root, child + else + make_inner_joins join_root, child + end } joins.concat outer_joins.flat_map { |oj| @@ -131,9 +136,9 @@ module ActiveRecord def instantiate(result_set, aliases) primary_key = aliases.column_alias(join_root, join_root.primary_key) - seen = Hash.new { |h,parent_klass| - h[parent_klass] = Hash.new { |i,parent_id| - i[parent_id] = Hash.new { |j,child_klass| j[child_klass] = {} } + seen = Hash.new { |i, object_id| + i[object_id] = Hash.new { |j, child_class| + j[child_class] = {} } } @@ -150,7 +155,8 @@ module ActiveRecord message_bus.instrument('instantiation.active_record', payload) do result_set.each { |row_hash| - parent = parents[row_hash[primary_key]] ||= join_root.instantiate(row_hash, column_aliases) + parent_key = primary_key ? row_hash[primary_key] : row_hash + parent = parents[parent_key] ||= join_root.instantiate(row_hash, column_aliases) construct(parent, join_root, row_hash, result_set, seen, model_cache, aliases) } end @@ -175,6 +181,14 @@ module ActiveRecord [info] + child.children.flat_map { |c| make_outer_joins(child, c) } end + def make_left_outer_joins(parent, child) + tables = child.tables + join_type = Arel::Nodes::OuterJoin + info = make_constraints parent, child, tables, join_type + + [info] + child.children.flat_map { |c| make_left_outer_joins(child, c) } + end + def make_inner_joins(parent, child) tables = child.tables join_type = Arel::Nodes::InnerJoin @@ -233,7 +247,6 @@ module ActiveRecord def construct(ar_parent, parent, row, rs, seen, model_cache, aliases) return if ar_parent.nil? - primary_id = ar_parent.id parent.children.each do |node| if node.reflection.collection? @@ -253,14 +266,14 @@ module ActiveRecord next end - model = seen[parent.base_klass][primary_id][node.base_klass][id] + model = seen[ar_parent.object_id][node.base_klass][id] if model construct(model, node, row, rs, seen, model_cache, aliases) else model = construct_model(ar_parent, node, row, model_cache, id, aliases) model.readonly! - seen[parent.base_klass][primary_id][node.base_klass][id] = model + seen[ar_parent.object_id][node.base_klass][id] = model construct(model, node, row, rs, seen, model_cache, aliases) end end diff --git a/activerecord/lib/active_record/associations/preloader.rb b/activerecord/lib/active_record/associations/preloader.rb index 3992a240b9..ecf6fb8643 100644 --- a/activerecord/lib/active_record/associations/preloader.rb +++ b/activerecord/lib/active_record/associations/preloader.rb @@ -54,6 +54,8 @@ module ActiveRecord autoload :BelongsTo, 'active_record/associations/preloader/belongs_to' end + NULL_RELATION = Struct.new(:values, :where_clause, :joins_values).new({}, Relation::WhereClause.empty, []) + # Eager loads the named associations for the given Active Record record(s). # # In this description, 'association name' shall refer to the name passed @@ -88,9 +90,6 @@ module ActiveRecord # [ :books, :author ] # { author: :avatar } # [ :books, { author: :avatar } ] - - NULL_RELATION = Struct.new(:values, :where_clause, :joins_values).new({}, Relation::WhereClause.empty, []) - def preload(records, associations, preload_scope = nil) records = Array.wrap(records).compact.uniq associations = Array.wrap(associations) @@ -107,6 +106,7 @@ module ActiveRecord private + # Loads all the given data into +records+ for the +association+. def preloaders_on(association, records, scope) case association when Hash @@ -132,6 +132,11 @@ module ActiveRecord } end + # Loads all the given data into +records+ for a singular +association+. + # + # Functions by instantiating a preloader class such as Preloader::HasManyThrough and + # call the +run+ method for each passed in class in the +records+ argument. + # # Not all records have the same class, so group then preload group on the reflection # itself so that if various subclass share the same association then we do not split # them unnecessarily @@ -181,6 +186,10 @@ module ActiveRecord def self.preloaded_records; []; end end + # Returns a class containing the logic needed to load preload the data + # and attach it to a relation. For example +Preloader::Association+ or + # +Preloader::HasManyThrough+. The class returned implements a `run` method + # that accepts a preloader. def preloader_for(reflection, owners, rhs_klass) return NullPreloader unless rhs_klass diff --git a/activerecord/lib/active_record/associations/preloader/association.rb b/activerecord/lib/active_record/associations/preloader/association.rb index 92792a7a15..e11a5cfb8a 100644 --- a/activerecord/lib/active_record/associations/preloader/association.rb +++ b/activerecord/lib/active_record/associations/preloader/association.rb @@ -12,7 +12,6 @@ module ActiveRecord @preload_scope = preload_scope @model = owners.first && owners.first.class @scope = nil - @owners_by_key = nil @preloaded_records = [] end @@ -56,18 +55,6 @@ module ActiveRecord raise NotImplementedError end - def owners_by_key - @owners_by_key ||= if key_conversion_required? - owners.group_by do |owner| - owner[owner_key_name].to_s - end - else - owners.group_by do |owner| - owner[owner_key_name] - end - end - end - def options reflection.options end @@ -75,32 +62,33 @@ module ActiveRecord private def associated_records_by_owner(preloader) - owners_map = owners_by_key - owner_keys = owners_map.keys.compact - - # Each record may have multiple owners, and vice-versa - records_by_owner = owners.each_with_object({}) do |owner,h| - h[owner] = [] + records = load_records + owners.each_with_object({}) do |owner, result| + result[owner] = records[convert_key(owner[owner_key_name])] || [] end + end - if owner_keys.any? - # 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(klass.connection.in_clause_length || owner_keys.size) - - records = load_slices sliced - records.each do |record, owner_key| - owners_map[owner_key].each do |owner| - records_by_owner[owner] << record - end + def owner_keys + unless defined?(@owner_keys) + @owner_keys = owners.map do |owner| + owner[owner_key_name] end + @owner_keys.uniq! + @owner_keys.compact! end - - records_by_owner + @owner_keys end def key_conversion_required? - association_key_type != owner_key_type + @key_conversion_required ||= association_key_type != owner_key_type + end + + def convert_key(key) + if key_conversion_required? + key.to_s + else + key + end end def association_key_type @@ -111,17 +99,17 @@ module ActiveRecord @model.type_for_attribute(owner_key_name.to_s).type end - def load_slices(slices) - @preloaded_records = slices.flat_map { |slice| + def load_records + return {} if owner_keys.empty? + # 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 + slices = owner_keys.each_slice(klass.connection.in_clause_length || owner_keys.size) + @preloaded_records = slices.flat_map do |slice| records_for(slice) - } - - @preloaded_records.map { |record| - key = record[association_key_name] - key = key.to_s if key_conversion_required? - - [record, key] - } + end + @preloaded_records.group_by do |record| + convert_key(record[association_key_name]) + end end def reflection_scope @@ -137,14 +125,23 @@ module ActiveRecord scope.where_clause = reflection_scope.where_clause + preload_scope.where_clause scope.references_values = Array(values[:references]) + Array(preload_values[:references]) - scope._select! preload_values[:select] || values[:select] || table[Arel.star] + if preload_values[:select] || values[:select] + scope._select!(preload_values[:select] || values[:select]) + end scope.includes! preload_values[:includes] || values[:includes] if preload_scope.joins_values.any? scope.joins!(preload_scope.joins_values) else scope.joins!(reflection_scope.joins_values) end - scope.order! preload_values[:order] || values[:order] + + if order_values = preload_values[:order] || values[:order] + scope.order!(order_values) + end + + if preload_values[:reordering] || values[:reordering] + scope.reordering_value = true + end if preload_values[:readonly] || values[:readonly] scope.readonly! diff --git a/activerecord/lib/active_record/associations/preloader/collection_association.rb b/activerecord/lib/active_record/associations/preloader/collection_association.rb index 5adffcd831..9939280fa4 100644 --- a/activerecord/lib/active_record/associations/preloader/collection_association.rb +++ b/activerecord/lib/active_record/associations/preloader/collection_association.rb @@ -2,13 +2,8 @@ module ActiveRecord module Associations class Preloader class CollectionAssociation < Association #:nodoc: - private - def build_scope - super.order(preload_scope.values[:order] || reflection_scope.values[:order]) - end - def preload(preloader) associated_records_by_owner(preloader).each do |owner, records| association = owner.association(reflection.name) @@ -17,7 +12,6 @@ module ActiveRecord records.each { |record| association.set_inverse_instance(record) } end end - end end end diff --git a/activerecord/lib/active_record/associations/preloader/has_one.rb b/activerecord/lib/active_record/associations/preloader/has_one.rb index 24728e9f01..c4add621ca 100644 --- a/activerecord/lib/active_record/associations/preloader/has_one.rb +++ b/activerecord/lib/active_record/associations/preloader/has_one.rb @@ -2,7 +2,6 @@ module ActiveRecord module Associations class Preloader class HasOne < SingularAssociation #:nodoc: - def association_key_name reflection.foreign_key end @@ -10,13 +9,6 @@ module ActiveRecord def owner_key_name reflection.active_record_primary_key end - - private - - def build_scope - super.order(preload_scope.values[:order] || reflection_scope.values[:order]) - end - end end end diff --git a/activerecord/lib/active_record/associations/preloader/through_association.rb b/activerecord/lib/active_record/associations/preloader/through_association.rb index 56aa23b173..24aa47bb51 100644 --- a/activerecord/lib/active_record/associations/preloader/through_association.rb +++ b/activerecord/lib/active_record/associations/preloader/through_association.rb @@ -84,7 +84,9 @@ module ActiveRecord end scope.references! reflection_scope.values[:references] - scope = scope.order reflection_scope.values[:order] if scope.eager_loading? + if scope.eager_loading? && order_values = reflection_scope.values[:order] + scope = scope.order(order_values) + end end scope diff --git a/activerecord/lib/active_record/associations/singular_association.rb b/activerecord/lib/active_record/associations/singular_association.rb index 03cb8cb8c3..c7cc48ba16 100644 --- a/activerecord/lib/active_record/associations/singular_association.rb +++ b/activerecord/lib/active_record/associations/singular_association.rb @@ -1,5 +1,3 @@ -require "active_support/deprecation" - module ActiveRecord module Associations class SingularAssociation < Association #:nodoc: diff --git a/activerecord/lib/active_record/attribute.rb b/activerecord/lib/active_record/attribute.rb index 74b44810d4..3c4c8f10ec 100644 --- a/activerecord/lib/active_record/attribute.rb +++ b/activerecord/lib/active_record/attribute.rb @@ -5,8 +5,8 @@ module ActiveRecord FromDatabase.new(name, value, type) end - def from_user(name, value, type) - FromUser.new(name, value, type) + def from_user(name, value, type, original_attribute = nil) + FromUser.new(name, value, type, original_attribute) end def with_cast_value(name, value, type) @@ -26,10 +26,11 @@ module ActiveRecord # This method should not be called directly. # Use #from_database or #from_user - def initialize(name, value_before_type_cast, type) + def initialize(name, value_before_type_cast, type, original_attribute = nil) @name = name @value_before_type_cast = value_before_type_cast @type = type + @original_attribute = original_attribute end def value @@ -38,21 +39,33 @@ module ActiveRecord @value end + def original_value + if assigned? + original_attribute.original_value + else + type_cast(value_before_type_cast) + end + end + def value_for_database type.serialize(value) end - def changed_from?(old_value) - type.changed?(old_value, value, value_before_type_cast) + def changed? + changed_from_assignment? || changed_in_place? + end + + def changed_in_place? + has_been_read? && type.changed_in_place?(original_value_for_database, value) end - def changed_in_place_from?(old_value) - has_been_read? && type.changed_in_place?(old_value, value) + def forgetting_assignment + with_value_from_database(value_for_database) end def with_value_from_user(value) type.assert_valid_value(value) - self.class.from_user(name, value, type) + self.class.from_user(name, value, type, self) end def with_value_from_database(value) @@ -64,7 +77,7 @@ module ActiveRecord end def with_type(type) - self.class.new(name, value_before_type_cast, type) + self.class.new(name, value_before_type_cast, type, original_attribute) end def type_cast(*) @@ -97,16 +110,39 @@ module ActiveRecord protected + attr_reader :original_attribute + alias_method :assigned?, :original_attribute + def initialize_dup(other) if defined?(@value) && @value.duplicable? @value = @value.dup end end + def changed_from_assignment? + assigned? && type.changed?(original_value, value, value_before_type_cast) + end + + def original_value_for_database + if assigned? + original_attribute.original_value_for_database + else + _original_value_for_database + end + end + + def _original_value_for_database + value_for_database + end + class FromDatabase < Attribute # :nodoc: def type_cast(value) type.deserialize(value) end + + def _original_value_for_database + value_before_type_cast + end end class FromUser < Attribute # :nodoc: diff --git a/activerecord/lib/active_record/attribute/user_provided_default.rb b/activerecord/lib/active_record/attribute/user_provided_default.rb index 501590cf0e..6dbd92ce28 100644 --- a/activerecord/lib/active_record/attribute/user_provided_default.rb +++ b/activerecord/lib/active_record/attribute/user_provided_default.rb @@ -2,10 +2,9 @@ require 'active_record/attribute' module ActiveRecord class Attribute # :nodoc: - class UserProvidedDefault < FromUser + class UserProvidedDefault < FromUser # :nodoc: def initialize(name, value, type, database_default) - super(name, value, type) - @database_default = database_default + super(name, value, type, database_default) end def type_cast(value) @@ -16,17 +15,9 @@ module ActiveRecord end end - def changed_from?(old_value) - super || changed_from?(database_default.value) - end - def with_type(type) - self.class.new(name, value_before_type_cast, type, database_default) + self.class.new(name, value_before_type_cast, type, original_attribute) end - - protected - - attr_reader :database_default end end end diff --git a/activerecord/lib/active_record/attribute_assignment.rb b/activerecord/lib/active_record/attribute_assignment.rb index 45fdcaa1cd..a6d81c82b4 100644 --- a/activerecord/lib/active_record/attribute_assignment.rb +++ b/activerecord/lib/active_record/attribute_assignment.rb @@ -5,7 +5,7 @@ module ActiveRecord extend ActiveSupport::Concern include ActiveModel::AttributeAssignment - # Alias for `assign_attributes`. See +ActiveModel::AttributeAssignment+. + # Alias for ActiveModel::AttributeAssignment#assign_attributes. See ActiveModel::AttributeAssignment. def attributes=(attributes) assign_attributes(attributes) end diff --git a/activerecord/lib/active_record/attribute_methods.rb b/activerecord/lib/active_record/attribute_methods.rb index ca6ba18fe0..4ae585d3f5 100644 --- a/activerecord/lib/active_record/attribute_methods.rb +++ b/activerecord/lib/active_record/attribute_methods.rb @@ -1,7 +1,7 @@ require 'active_support/core_ext/enumerable' require 'active_support/core_ext/string/filters' require 'mutex_m' -require 'concurrent' +require 'concurrent/map' module ActiveRecord # = Active Record Attribute Methods @@ -96,7 +96,7 @@ module ActiveRecord end end - # Raises an <tt>ActiveRecord::DangerousAttributeError</tt> exception when an + # Raises an ActiveRecord::DangerousAttributeError exception when an # \Active \Record method is defined in the model, otherwise +false+. # # class Person < ActiveRecord::Base @@ -346,7 +346,7 @@ module ActiveRecord # # Note: +:id+ is always present. # - # Alias for the <tt>read_attribute</tt> method. + # Alias for the #read_attribute method. # # class Person < ActiveRecord::Base # belongs_to :organization @@ -364,7 +364,7 @@ module ActiveRecord end # Updates the attribute identified by <tt>attr_name</tt> with the specified +value+. - # (Alias for the protected <tt>write_attribute</tt> method). + # (Alias for the protected #write_attribute method). # # class Person < ActiveRecord::Base # end diff --git a/activerecord/lib/active_record/attribute_methods/before_type_cast.rb b/activerecord/lib/active_record/attribute_methods/before_type_cast.rb index 56c1898551..1db6776688 100644 --- a/activerecord/lib/active_record/attribute_methods/before_type_cast.rb +++ b/activerecord/lib/active_record/attribute_methods/before_type_cast.rb @@ -2,7 +2,7 @@ module ActiveRecord module AttributeMethods # = Active Record Attribute Methods Before Type Cast # - # <tt>ActiveRecord::AttributeMethods::BeforeTypeCast</tt> provides a way to + # ActiveRecord::AttributeMethods::BeforeTypeCast provides a way to # read the value of the attributes before typecasting and deserialization. # # class Task < ActiveRecord::Base diff --git a/activerecord/lib/active_record/attribute_methods/dirty.rb b/activerecord/lib/active_record/attribute_methods/dirty.rb index 43c15841c2..0bcfa5f00d 100644 --- a/activerecord/lib/active_record/attribute_methods/dirty.rb +++ b/activerecord/lib/active_record/attribute_methods/dirty.rb @@ -35,23 +35,22 @@ module ActiveRecord # <tt>reload</tt> the record and clears changed attributes. def reload(*) super.tap do - clear_changes_information + @mutation_tracker = nil + @previous_mutation_tracker = nil + @changed_attributes = HashWithIndifferentAccess.new end end - def init_internals - super - @mutation_tracker = AttributeMutationTracker.new(@attributes, @attributes.dup) - end - def initialize_dup(other) # :nodoc: super - @mutation_tracker = AttributeMutationTracker.new(@attributes, - self.class._default_attributes.deep_dup) + @attributes = self.class._default_attributes.map do |attr| + attr.with_value_from_user(@attributes.fetch_value(attr.name)) + end + @mutation_tracker = nil end def changes_applied - @previous_mutation_tracker = @mutation_tracker + @previous_mutation_tracker = mutation_tracker @changed_attributes = HashWithIndifferentAccess.new store_original_attributes end @@ -81,7 +80,7 @@ module ActiveRecord if defined?(@cached_changed_attributes) @cached_changed_attributes else - super.reverse_merge(@mutation_tracker.changed_values).freeze + super.reverse_merge(mutation_tracker.changed_values).freeze end end @@ -96,17 +95,24 @@ module ActiveRecord end def attribute_changed_in_place?(attr_name) - @mutation_tracker.changed_in_place?(attr_name) + mutation_tracker.changed_in_place?(attr_name) end private + def mutation_tracker + unless defined?(@mutation_tracker) + @mutation_tracker = nil + end + @mutation_tracker ||= AttributeMutationTracker.new(@attributes) + end + def changes_include?(attr_name) - super || @mutation_tracker.changed?(attr_name) + super || mutation_tracker.changed?(attr_name) end def clear_attribute_change(attr_name) - @mutation_tracker.forget_change(attr_name) + mutation_tracker.forget_change(attr_name) end def _update_record(*) @@ -122,11 +128,12 @@ module ActiveRecord end def store_original_attributes - @mutation_tracker = @mutation_tracker.now_tracking(@attributes) + @attributes = @attributes.map(&:forgetting_assignment) + @mutation_tracker = nil end def previous_mutation_tracker - @previous_mutation_tracker ||= NullMutationTracker.new + @previous_mutation_tracker ||= NullMutationTracker.instance end def cache_changed_attributes diff --git a/activerecord/lib/active_record/attribute_methods/primary_key.rb b/activerecord/lib/active_record/attribute_methods/primary_key.rb index c28374e4ab..0d5cb8b37c 100644 --- a/activerecord/lib/active_record/attribute_methods/primary_key.rb +++ b/activerecord/lib/active_record/attribute_methods/primary_key.rb @@ -5,7 +5,7 @@ module ActiveRecord module PrimaryKey extend ActiveSupport::Concern - # Returns this record's primary key value wrapped in an Array if one is + # Returns this record's primary key value wrapped in an array if one is # available. def to_key sync_with_transaction_state @@ -108,7 +108,7 @@ module ActiveRecord # self.primary_key = 'sysid' # end # - # You can also define the +primary_key+ method yourself: + # You can also define the #primary_key method yourself: # # class Project < ActiveRecord::Base # def self.primary_key diff --git a/activerecord/lib/active_record/attribute_methods/serialization.rb b/activerecord/lib/active_record/attribute_methods/serialization.rb index 60eecab0d0..65978aea2a 100644 --- a/activerecord/lib/active_record/attribute_methods/serialization.rb +++ b/activerecord/lib/active_record/attribute_methods/serialization.rb @@ -9,7 +9,7 @@ module ActiveRecord # 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 assignment and retrieval. - # Otherwise <tt>SerializationTypeMismatch</tt> will be raised. + # Otherwise SerializationTypeMismatch will be raised. # # Empty objects as <tt>{}</tt>, in the case of +Hash+, or <tt>[]</tt>, in the case of # +Array+, will always be persisted as null. @@ -17,7 +17,7 @@ module ActiveRecord # Keep in mind that database adapters handle certain serialization tasks # for you. For instance: +json+ and +jsonb+ types in PostgreSQL will be # converted between JSON object/array syntax and Ruby +Hash+ or +Array+ - # objects transparently. There is no need to use +serialize+ in this + # objects transparently. There is no need to use #serialize in this # case. # # For more complex cases, such as conversion to or from your application diff --git a/activerecord/lib/active_record/attribute_mutation_tracker.rb b/activerecord/lib/active_record/attribute_mutation_tracker.rb index 08eb8ba7ac..0133b4d0be 100644 --- a/activerecord/lib/active_record/attribute_mutation_tracker.rb +++ b/activerecord/lib/active_record/attribute_mutation_tracker.rb @@ -1,14 +1,13 @@ module ActiveRecord class AttributeMutationTracker # :nodoc: - def initialize(attributes, original_attributes) + def initialize(attributes) @attributes = attributes - @original_attributes = original_attributes end def changed_values attr_names.each_with_object({}.with_indifferent_access) do |attr_name, result| if changed?(attr_name) - result[attr_name] = original_attributes.fetch_value(attr_name) + result[attr_name] = attributes[attr_name].original_value end end end @@ -16,52 +15,39 @@ module ActiveRecord def changes attr_names.each_with_object({}.with_indifferent_access) do |attr_name, result| if changed?(attr_name) - result[attr_name] = [original_attributes.fetch_value(attr_name), attributes.fetch_value(attr_name)] + result[attr_name] = [attributes[attr_name].original_value, attributes.fetch_value(attr_name)] end end end def changed?(attr_name) attr_name = attr_name.to_s - modified?(attr_name) || changed_in_place?(attr_name) + attributes[attr_name].changed? end def changed_in_place?(attr_name) - original_database_value = original_attributes[attr_name].value_before_type_cast - attributes[attr_name].changed_in_place_from?(original_database_value) + attributes[attr_name].changed_in_place? end def forget_change(attr_name) attr_name = attr_name.to_s - original_attributes[attr_name] = attributes[attr_name].dup - end - - def now_tracking(new_attributes) - AttributeMutationTracker.new(new_attributes, clean_copy_of(new_attributes)) + attributes[attr_name] = attributes[attr_name].forgetting_assignment end protected - attr_reader :attributes, :original_attributes + attr_reader :attributes private def attr_names attributes.keys end - - def modified?(attr_name) - attributes[attr_name].changed_from?(original_attributes.fetch_value(attr_name)) - end - - def clean_copy_of(attributes) - attributes.map do |attr| - attr.with_value_from_database(attr.value_for_database) - end - end end class NullMutationTracker # :nodoc: + include Singleton + def changed_values {} end diff --git a/activerecord/lib/active_record/attribute_set.rb b/activerecord/lib/active_record/attribute_set.rb index 026b3cf014..be581ac2a9 100644 --- a/activerecord/lib/active_record/attribute_set.rb +++ b/activerecord/lib/active_record/attribute_set.rb @@ -91,6 +91,10 @@ module ActiveRecord AttributeSet.new(new_attributes) end + def ==(other) + attributes == other.attributes + end + protected attr_reader :attributes diff --git a/activerecord/lib/active_record/attribute_set/builder.rb b/activerecord/lib/active_record/attribute_set/builder.rb index f974b7a876..3bd7c7997b 100644 --- a/activerecord/lib/active_record/attribute_set/builder.rb +++ b/activerecord/lib/active_record/attribute_set/builder.rb @@ -68,10 +68,29 @@ module ActiveRecord end end + def ==(other) + if other.is_a?(LazyAttributeHash) + materialize == other.materialize + else + materialize == other + end + end + protected attr_reader :types, :values, :additional_types, :delegate_hash + def materialize + unless @materialized + values.each_key { |key| self[key] } + types.each_key { |key| self[key] } + unless frozen? + @materialized = true + end + end + delegate_hash + end + private def assign_default_value(name) @@ -85,16 +104,5 @@ module ActiveRecord delegate_hash[name] = Attribute.uninitialized(name, type) end end - - def materialize - unless @materialized - values.each_key { |key| self[key] } - types.each_key { |key| self[key] } - unless frozen? - @materialized = true - end - end - delegate_hash - end end end diff --git a/activerecord/lib/active_record/attributes.rb b/activerecord/lib/active_record/attributes.rb index 8b2c4c7170..5d0405c3be 100644 --- a/activerecord/lib/active_record/attributes.rb +++ b/activerecord/lib/active_record/attributes.rb @@ -5,9 +5,6 @@ module ActiveRecord module Attributes extend ActiveSupport::Concern - # :nodoc: - Type = ActiveRecord::Type - included do class_attribute :attributes_to_define_after_schema_loads, instance_accessor: false # :internal: self.attributes_to_define_after_schema_loads = {} @@ -18,7 +15,7 @@ module ActiveRecord # type of existing attributes if needed. This allows control over how # values are converted to and from SQL when assigned to a model. It also # changes the behavior of values passed to - # ActiveRecord::QueryMethods#where. This will let you use + # {ActiveRecord::Base.where}[rdoc-ref:QueryMethods#where]. This will let you use # your domain objects across much of Active Record, without having to # rely on implementation details or monkey patching. # @@ -90,7 +87,7 @@ module ActiveRecord # sleep 1 # Product.new.my_default_proc # => 2015-05-30 11:04:49 -0600 # - # Attributes do not need to be backed by a database column. + # \Attributes do not need to be backed by a database column. # # class MyModel < ActiveRecord::Base # attribute :my_string, :string @@ -122,7 +119,7 @@ module ActiveRecord # # class MoneyType < ActiveRecord::Type::Integer # def cast(value) - # if value.include?('$') + # if !value.kind_of(Numeric) && value.include?('$') # price_in_dollars = value.gsub(/\$/, '').to_f # super(price_in_dollars * 100) # else @@ -147,9 +144,9 @@ module ActiveRecord # to be referenced by a symbol, see ActiveRecord::Type.register. You can # also pass a type object directly, in place of a symbol. # - # ==== Querying + # ==== \Querying # - # When ActiveRecord::QueryMethods#where is called, it will + # When {ActiveRecord::Base.where}[rdoc-ref:QueryMethods#where] is called, it will # use the type defined by the model class to convert the value to SQL, # calling +serialize+ on your type object. For example: # diff --git a/activerecord/lib/active_record/autosave_association.rb b/activerecord/lib/active_record/autosave_association.rb index d0de42d27c..fc12c3f45a 100644 --- a/activerecord/lib/active_record/autosave_association.rb +++ b/activerecord/lib/active_record/autosave_association.rb @@ -1,10 +1,10 @@ module ActiveRecord # = Active Record Autosave Association # - # +AutosaveAssociation+ is a module that takes care of automatically saving + # AutosaveAssociation is a module that takes care of automatically saving # associated records when their parent is saved. In addition to saving, it # also destroys any associated records that were marked for destruction. - # (See +mark_for_destruction+ and <tt>marked_for_destruction?</tt>). + # (See #mark_for_destruction and #marked_for_destruction?). # # Saving of the parent, its associations, and the destruction of marked # associations, all happen inside a transaction. This should never leave the @@ -125,7 +125,6 @@ module ActiveRecord # Now it _is_ removed from the database: # # Comment.find_by(id: id).nil? # => true - module AutosaveAssociation extend ActiveSupport::Concern @@ -141,9 +140,11 @@ module ActiveRecord included do Associations::Builder::Association.extensions << AssociationBuilderExtension + mattr_accessor :index_nested_attribute_errors, instance_writer: false + self.index_nested_attribute_errors = false end - module ClassMethods + module ClassMethods # :nodoc: private def define_non_cyclic_method(name, &block) @@ -316,7 +317,7 @@ module ActiveRecord def validate_collection_association(reflection) if association = association_instance_get(reflection.name) if records = associated_records_to_validate_or_save(association, new_record?, reflection.options[:autosave]) - records.each { |record| association_valid?(reflection, record) } + records.each_with_index { |record, index| association_valid?(reflection, record, index) } end end end @@ -324,14 +325,18 @@ module ActiveRecord # Returns whether or not the association is valid and applies any errors to # the parent, <tt>self</tt>, if it wasn't. Skips any <tt>:autosave</tt> # enabled records if they're marked_for_destruction? or destroyed. - def association_valid?(reflection, record) + def association_valid?(reflection, record, index=nil) return true if record.destroyed? || (reflection.options[:autosave] && record.marked_for_destruction?) validation_context = self.validation_context unless [:create, :update].include?(self.validation_context) unless valid = record.valid?(validation_context) if reflection.options[:autosave] record.errors.each do |attribute, message| - attribute = "#{reflection.name}.#{attribute}" + if index.nil? || (!reflection.options[:index_errors] && !ActiveRecord::Base.index_nested_attribute_errors) + attribute = "#{reflection.name}.#{attribute}" + else + attribute = "#{reflection.name}[#{index}].#{attribute}" + end errors[attribute] << message errors[attribute].uniq! end @@ -353,7 +358,7 @@ module ActiveRecord # <tt>:autosave</tt> is enabled on the association. # # In addition, it destroys all children that were marked for destruction - # with mark_for_destruction. + # with #mark_for_destruction. # # This all happens inside a transaction, _if_ the Transactions module is included into # ActiveRecord::Base after the AutosaveAssociation module, which it does by default. @@ -396,7 +401,7 @@ module ActiveRecord # on the association. # # In addition, it will destroy the association if it was marked for - # destruction with mark_for_destruction. + # destruction with #mark_for_destruction. # # This all happens inside a transaction, _if_ the Transactions module is included into # ActiveRecord::Base after the AutosaveAssociation module, which it does by default. diff --git a/activerecord/lib/active_record/base.rb b/activerecord/lib/active_record/base.rb index 4b66d8cd36..9782e58299 100644 --- a/activerecord/lib/active_record/base.rb +++ b/activerecord/lib/active_record/base.rb @@ -170,7 +170,7 @@ module ActiveRecord #:nodoc: # <tt>Person.find_by_user_name(user_name)</tt>. # # It's possible to add an exclamation point (!) on the end of the dynamic finders to get them to raise an - # <tt>ActiveRecord::RecordNotFound</tt> error if they do not return any records, + # ActiveRecord::RecordNotFound error if they do not return any records, # like <tt>Person.find_by_last_name!</tt>. # # It's also possible to use multiple attributes in the same find by separating them with "_and_". @@ -185,7 +185,8 @@ module ActiveRecord #:nodoc: # == Saving arrays, hashes, and other non-mappable objects in text columns # # Active Record can serialize any object in text columns using YAML. To do so, you must - # specify this with a call to the class method +serialize+. + # specify this with a call to the class method + # {serialize}[rdoc-ref:AttributeMethods::Serialization::ClassMethods#serialize]. # This makes it possible to store arrays, hashes, and other non-mappable objects without doing # any additional work. # @@ -225,39 +226,47 @@ module ActiveRecord #:nodoc: # # == Connection to multiple databases in different models # - # Connections are usually created through ActiveRecord::Base.establish_connection and retrieved + # Connections are usually created through + # {ActiveRecord::Base.establish_connection}[rdoc-ref:ConnectionHandling#establish_connection] and retrieved # by ActiveRecord::Base.connection. All classes inheriting from ActiveRecord::Base will use this # connection. But you can also set a class-specific connection. For example, if Course is an # ActiveRecord::Base, but resides in a different database, you can just say <tt>Course.establish_connection</tt> # and Course and all of its subclasses will use this connection instead. # # This feature is implemented by keeping a connection pool in ActiveRecord::Base that is - # a Hash indexed by the class. If a connection is requested, the retrieve_connection method + # a hash indexed by the class. If a connection is requested, the + # {ActiveRecord::Base.retrieve_connection}[rdoc-ref:ConnectionHandling#retrieve_connection] method # will go up the class-hierarchy until a connection is found in the connection pool. # # == Exceptions # # * ActiveRecordError - Generic error class and superclass of all other errors raised by Active Record. - # * AdapterNotSpecified - The configuration hash used in <tt>establish_connection</tt> didn't include an - # <tt>:adapter</tt> key. - # * AdapterNotFound - The <tt>:adapter</tt> key used in <tt>establish_connection</tt> specified a - # non-existent adapter + # * AdapterNotSpecified - The configuration hash used in + # {ActiveRecord::Base.establish_connection}[rdoc-ref:ConnectionHandling#establish_connection] + # didn't include an <tt>:adapter</tt> key. + # * AdapterNotFound - The <tt>:adapter</tt> key used in + # {ActiveRecord::Base.establish_connection}[rdoc-ref:ConnectionHandling#establish_connection] + # specified a non-existent adapter # (or a bad spelling of an existing one). # * AssociationTypeMismatch - The object assigned to the association wasn't of the type # specified in the association definition. # * AttributeAssignmentError - An error occurred while doing a mass assignment through the - # <tt>attributes=</tt> method. + # {ActiveRecord::Base#attributes=}[rdoc-ref:AttributeAssignment#attributes=] 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. + # * ConnectionNotEstablished - No connection has been established. + # Use {ActiveRecord::Base.establish_connection}[rdoc-ref:ConnectionHandling#establish_connection] before querying. # * 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 + # {ActiveRecord::Base#attributes=}[rdoc-ref:AttributeAssignment#attributes=] method. + # The +errors+ property of this exception contains an array of # AttributeAssignmentError # objects that should be inspected to determine which attributes triggered the errors. - # * RecordInvalid - raised by <tt>save!</tt> and <tt>create!</tt> 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 + # * RecordInvalid - raised by {ActiveRecord::Base#save}[rdoc-ref:Persistence#save] and + # {ActiveRecord::Base.create!}[rdoc-ref:Persistence::ClassMethods#create!] + # when the record is invalid. + # * RecordNotFound - No record responded to the {ActiveRecord::Base.find}[rdoc-ref:FinderMethods#find] method. + # Either the row with the given ID doesn't exist or the row didn't meet the additional restrictions. + # Some {ActiveRecord::Base.find}[rdoc-ref:FinderMethods#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. diff --git a/activerecord/lib/active_record/callbacks.rb b/activerecord/lib/active_record/callbacks.rb index ccdbebbc77..cfd8cbda67 100644 --- a/activerecord/lib/active_record/callbacks.rb +++ b/activerecord/lib/active_record/callbacks.rb @@ -1,11 +1,11 @@ module ActiveRecord - # = Active Record Callbacks + # = Active Record \Callbacks # - # Callbacks are hooks into the life cycle of an Active Record object that allow you to trigger logic + # \Callbacks are hooks into the life cycle of an Active Record object that allow you to trigger logic # before or after an alteration of the object state. This can be used to make sure that associated and - # dependent objects are deleted when +destroy+ is called (by overwriting +before_destroy+) or to massage attributes - # before they're validated (by overwriting +before_validation+). As an example of the callbacks initiated, consider - # the <tt>Base#save</tt> call for a new record: + # dependent objects are deleted when {ActiveRecord::Base#destroy}[rdoc-ref:Persistence#destroy] is called (by overwriting +before_destroy+) or + # to massage attributes before they're validated (by overwriting +before_validation+). + # As an example of the callbacks initiated, consider the {ActiveRecord::Base#save}[rdoc-ref:Persistence#save] call for a new record: # # * (-) <tt>save</tt> # * (-) <tt>valid</tt> @@ -20,7 +20,7 @@ module ActiveRecord # * (7) <tt>after_commit</tt> # # Also, an <tt>after_rollback</tt> callback can be configured to be triggered whenever a rollback is issued. - # Check out <tt>ActiveRecord::Transactions</tt> for more details about <tt>after_commit</tt> and + # Check out ActiveRecord::Transactions for more details about <tt>after_commit</tt> and # <tt>after_rollback</tt>. # # Additionally, an <tt>after_touch</tt> callback is triggered whenever an @@ -31,7 +31,7 @@ module ActiveRecord # are instantiated as well. # # There are nineteen callbacks in total, which give you immense power to react and prepare for each state in the - # Active Record life cycle. The sequence for calling <tt>Base#save</tt> for an existing record is similar, + # Active Record life cycle. The sequence for calling {ActiveRecord::Base#save}[rdoc-ref:Persistence#save] for an existing record is similar, # except that each <tt>_create</tt> callback is replaced by the corresponding <tt>_update</tt> callback. # # Examples: @@ -193,8 +193,9 @@ module ActiveRecord # == <tt>before_validation*</tt> returning statements # # If the +before_validation+ callback throws +:abort+, the process will be - # aborted and <tt>Base#save</tt> will return +false+. If Base#save! is called it will raise a - # <tt>ActiveRecord::RecordInvalid</tt> exception. Nothing will be appended to the errors object. + # aborted and {ActiveRecord::Base#save}[rdoc-ref:Persistence#save] will return +false+. + # If {ActiveRecord::Base#save!}[rdoc-ref:Persistence#save!] is called it will raise a ActiveRecord::RecordInvalid exception. + # Nothing will be appended to the errors object. # # == Canceling callbacks # @@ -223,7 +224,8 @@ module ActiveRecord # end # # In this case, the problem is that when the +before_destroy+ callback is executed, the children are not available - # because the +destroy+ callback gets executed first. You can use the +prepend+ option on the +before_destroy+ callback to avoid this. + # because the {ActiveRecord::Base#destroy}[rdoc-ref:Persistence#destroy] callback gets executed first. + # You can use the +prepend+ option on the +before_destroy+ callback to avoid this. # # class Topic < ActiveRecord::Base # has_many :children, dependent: destroy @@ -238,21 +240,21 @@ module ActiveRecord # # This way, the +before_destroy+ gets executed before the <tt>dependent: destroy</tt> is called, and the data is still available. # - # == Transactions + # == \Transactions # - # The entire callback chain of a +save+, <tt>save!</tt>, or +destroy+ call runs - # within a transaction. That includes <tt>after_*</tt> hooks. If everything - # goes fine a COMMIT is executed once the chain has been completed. + # The entire callback chain of a {#save}[rdoc-ref:Persistence#save], {#save!}[rdoc-ref:Persistence#save!], + # or {#destroy}[rdoc-ref:Persistence#destroy] call runs within a transaction. That includes <tt>after_*</tt> hooks. + # If everything goes fine a COMMIT is executed once the chain has been completed. # # If a <tt>before_*</tt> callback cancels the action a ROLLBACK is issued. You # can also trigger a ROLLBACK raising an exception in any of the callbacks, # including <tt>after_*</tt> hooks. Note, however, that in that case the client - # needs to be aware of it because an ordinary +save+ will raise such exception + # needs to be aware of it because an ordinary {#save}[rdoc-ref:Persistence#save] will raise such exception # instead of quietly returning +false+. # # == Debugging callbacks # - # The callback chain is accessible via the <tt>_*_callbacks</tt> method on an object. ActiveModel Callbacks support + # The callback chain is accessible via the <tt>_*_callbacks</tt> method on an object. Active Model \Callbacks support # <tt>:before</tt>, <tt>:after</tt> and <tt>:around</tt> as values for the <tt>kind</tt> property. The <tt>kind</tt> property # defines what part of the chain the callback runs in. # @@ -278,7 +280,7 @@ module ActiveRecord :before_destroy, :around_destroy, :after_destroy, :after_commit, :after_rollback ] - module ClassMethods + module ClassMethods # :nodoc: include ActiveModel::Callbacks end @@ -290,10 +292,15 @@ module ActiveRecord end def destroy #:nodoc: + @_destroy_callback_already_called ||= false + return if @_destroy_callback_already_called + @_destroy_callback_already_called = true _run_destroy_callbacks { super } rescue RecordNotDestroyed => e @_association_destroy_exception = e false + ensure + @_destroy_callback_already_called = false end def touch(*) #:nodoc: 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 b579bc1e93..486b7b6d25 100644 --- a/activerecord/lib/active_record/connection_adapters/abstract/connection_pool.rb +++ b/activerecord/lib/active_record/connection_adapters/abstract/connection_pool.rb @@ -1,5 +1,5 @@ require 'thread' -require 'concurrent' +require 'concurrent/map' require 'monitor' module ActiveRecord @@ -10,8 +10,9 @@ module ActiveRecord end # Raised when a pool was unable to get ahold of all its connections - # to perform a "group" action such as +ConnectionPool#disconnect!+ - # or +ConnectionPool#clear_reloadable_connections!+. + # to perform a "group" action such as + # {ActiveRecord::Base.connection_pool.disconnect!}[rdoc-ref:ConnectionAdapters::ConnectionPool#disconnect!] + # or {ActiveRecord::Base.clear_reloadable_connections!}[rdoc-ref:ConnectionAdapters::ConnectionHandler#clear_reloadable_connections!]. class ExclusiveConnectionTimeoutError < ConnectionTimeoutError end @@ -37,17 +38,18 @@ module ActiveRecord # Connections can be obtained and used from a connection pool in several # ways: # - # 1. Simply use ActiveRecord::Base.connection as with Active Record 2.1 and + # 1. Simply use {ActiveRecord::Base.connection}[rdoc-ref:ConnectionHandling.connection] + # as with Active Record 2.1 and # earlier (pre-connection-pooling). Eventually, when you're done with # the connection(s) and wish it to be returned to the pool, you call - # ActiveRecord::Base.clear_active_connections!. This will be the - # default behavior for Active Record when used in conjunction with + # {ActiveRecord::Base.clear_active_connections!}[rdoc-ref:ConnectionAdapters::ConnectionHandler#clear_active_connections!]. + # This will be the default behavior for Active Record when used in conjunction with # Action Pack's request handling cycle. # 2. Manually check out a connection from the pool with - # ActiveRecord::Base.connection_pool.checkout. You are responsible for + # {ActiveRecord::Base.connection_pool.checkout}[rdoc-ref:#checkout]. You are responsible for # returning this connection to the pool when finished by calling - # ActiveRecord::Base.connection_pool.checkin(connection). - # 3. Use ActiveRecord::Base.connection_pool.with_connection(&block), which + # {ActiveRecord::Base.connection_pool.checkin(connection)}[rdoc-ref:#checkin]. + # 3. Use {ActiveRecord::Base.connection_pool.with_connection(&block)}[rdoc-ref:#with_connection], which # obtains a connection, yields it as the sole argument to the block, # and returns it to the pool after the block completes. # @@ -140,7 +142,7 @@ module ActiveRecord # become available. # # Raises: - # - ConnectionTimeoutError if +timeout+ is given and no element + # - ActiveRecord::ConnectionTimeoutError if +timeout+ is given and no element # becomes available within +timeout+ seconds, def poll(timeout = nil) synchronize { internal_poll(timeout) } @@ -331,7 +333,7 @@ module ActiveRecord # of the cache is to speed-up +connection+ method, it is not the authoritative # registry of which thread owns which connection, that is tracked by # +connection.owner+ attr on each +connection+ instance. - # The invariant works like this: if there is mapping of +thread => conn+, + # The invariant works like this: if there is mapping of <tt>thread => conn</tt>, # then that +thread+ does indeed own that +conn+, however an absence of a such # mapping does not mean that the +thread+ doesn't own the said connection, in # that case +conn.owner+ attr should be consulted. @@ -342,7 +344,7 @@ module ActiveRecord @connections = [] @automatic_reconnect = true - # Connection pool allows for concurrent (outside the main `synchronize` section) + # Connection pool allows for concurrent (outside the main +synchronize+ section) # establishment of new connections. This variable tracks the number of threads # currently in the process of independently establishing connections to the DB. @now_connecting = 0 @@ -406,9 +408,9 @@ module ActiveRecord # Disconnects all connections in the pool, and clears the pool. # # Raises: - # - +ExclusiveConnectionTimeoutError+ if unable to gain ownership of all + # - ActiveRecord::ExclusiveConnectionTimeoutError if unable to gain ownership of all # connections in the pool within a timeout interval (default duration is - # +spec.config[:checkout_timeout] * 2+ seconds). + # <tt>spec.config[:checkout_timeout] * 2</tt> seconds). def disconnect(raise_on_acquisition_timeout = true) with_exclusively_acquired_all_connections(raise_on_acquisition_timeout) do synchronize do @@ -426,7 +428,7 @@ module ActiveRecord # # The pool first tries to gain ownership of all connections, if unable to # do so within a timeout interval (default duration is - # +spec.config[:checkout_timeout] * 2+ seconds), the pool is forcefully + # <tt>spec.config[:checkout_timeout] * 2</tt> seconds), the pool is forcefully # disconnected without any regard for other connection owning threads. def disconnect! disconnect(false) @@ -436,9 +438,9 @@ module ActiveRecord # require reloading. # # Raises: - # - +ExclusiveConnectionTimeoutError+ if unable to gain ownership of all + # - ActiveRecord::ExclusiveConnectionTimeoutError if unable to gain ownership of all # connections in the pool within a timeout interval (default duration is - # +spec.config[:checkout_timeout] * 2+ seconds). + # <tt>spec.config[:checkout_timeout] * 2</tt> seconds). def clear_reloadable_connections(raise_on_acquisition_timeout = true) num_new_conns_required = 0 @@ -474,7 +476,7 @@ module ActiveRecord # # The pool first tries to gain ownership of all connections, if unable to # do so within a timeout interval (default duration is - # +spec.config[:checkout_timeout] * 2+ seconds), the pool forcefully + # <tt>spec.config[:checkout_timeout] * 2</tt> seconds), the pool forcefully # clears the cache and reloads connections without any regard for other # connection owning threads. def clear_reloadable_connections! @@ -494,7 +496,7 @@ module ActiveRecord # Returns: an AbstractAdapter object. # # Raises: - # - ConnectionTimeoutError: no connection can be obtained from the pool. + # - ActiveRecord::ConnectionTimeoutError no connection can be obtained from the pool. def checkout(checkout_timeout = @checkout_timeout) checkout_and_verify(acquire_connection(checkout_timeout)) end @@ -503,7 +505,7 @@ module ActiveRecord # no longer need this connection. # # +conn+: an AbstractAdapter object, which was obtained by earlier by - # calling +checkout+ on this pool. + # calling #checkout on this pool. def checkin(conn) synchronize do remove_connection_from_thread_cache conn @@ -516,7 +518,7 @@ module ActiveRecord end end - # Remove a connection from the connection pool. The connection will + # Remove a connection from the connection pool. The connection will # remain open and active but will no longer be managed by this pool. def remove(conn) needs_new_connection = false @@ -547,7 +549,7 @@ module ActiveRecord bulk_make_new_connections(1) if needs_new_connection end - # Recover lost connections for the pool. A lost connection can occur if + # Recover lost connections for the pool. A lost connection can occur if # a programmer forgets to checkin a connection at the end of a thread # or a thread dies unexpectedly. def reap @@ -628,10 +630,10 @@ module ActiveRecord end end rescue ExclusiveConnectionTimeoutError - # `raise_on_acquisition_timeout == false` means we are directed to ignore any + # <tt>raise_on_acquisition_timeout == false</tt> means we are directed to ignore any # timeouts and are expected to just give up: we've obtained as many connections # as possible, note that in a case like that we don't return any of the - # `newly_checked_out` connections. + # +newly_checked_out+ connections. if raise_on_acquisition_timeout release_newly_checked_out = true @@ -688,18 +690,18 @@ module ActiveRecord # queue for a connection to become available. # # Raises: - # - ConnectionTimeoutError if a connection could not be acquired + # - ActiveRecord::ConnectionTimeoutError if a connection could not be acquired # #-- # Implementation detail: the connection returned by +acquire_connection+ # will already be "+connection.lease+ -ed" to the current thread. def acquire_connection(checkout_timeout) - # NOTE: we rely on `@available.poll` and `try_to_checkout_new_connection` to - # `conn.lease` the returned connection (and to do this in a `synchronized` + # NOTE: we rely on +@available.poll+ and +try_to_checkout_new_connection+ to + # +conn.lease+ the returned connection (and to do this in a +synchronized+ # section), this is not the cleanest implementation, as ideally we would - # `synchronize { conn.lease }` in this method, but by leaving it to `@available.poll` - # and `try_to_checkout_new_connection` we can piggyback on `synchronize` sections - # of the said methods and avoid an additional `synchronize` overhead. + # <tt>synchronize { conn.lease }</tt> in this method, but by leaving it to +@available.poll+ + # and +try_to_checkout_new_connection+ we can piggyback on +synchronize+ sections + # of the said methods and avoid an additional +synchronize+ overhead. if conn = @available.poll || try_to_checkout_new_connection conn else @@ -857,6 +859,8 @@ module ActiveRecord end # Clears the cache which maps classes. + # + # See ConnectionPool#clear_reloadable_connections! for details. def clear_reloadable_connections! connection_pool_list.each(&:clear_reloadable_connections!) end @@ -956,12 +960,11 @@ module ActiveRecord def call(env) testing = env['rack.test'] - response = @app.call(env) - response[2] = ::Rack::BodyProxy.new(response[2]) do + status, headers, body = @app.call(env) + proxy = ::Rack::BodyProxy.new(body) do ActiveRecord::Base.clear_active_connections! unless testing end - - response + [status, headers, proxy] rescue Exception ActiveRecord::Base.clear_active_connections! unless testing raise diff --git a/activerecord/lib/active_record/connection_adapters/abstract/database_limits.rb b/activerecord/lib/active_record/connection_adapters/abstract/database_limits.rb index 30b2fca2ca..6711049588 100644 --- a/activerecord/lib/active_record/connection_adapters/abstract/database_limits.rb +++ b/activerecord/lib/active_record/connection_adapters/abstract/database_limits.rb @@ -19,8 +19,8 @@ module ActiveRecord # Returns the maximum allowed length for an index name. This # limit is enforced by \Rails and is less than or equal to - # <tt>index_name_length</tt>. The gap between - # <tt>index_name_length</tt> is to allow internal \Rails + # #index_name_length. The gap between + # #index_name_length is to allow internal \Rails # operations to use prefixes in temporary operations. def allowed_index_name_length index_name_length 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 107806cd93..848aeb821c 100644 --- a/activerecord/lib/active_record/connection_adapters/abstract/database_statements.rb +++ b/activerecord/lib/active_record/connection_adapters/abstract/database_statements.rb @@ -29,7 +29,17 @@ module ActiveRecord # Returns an ActiveRecord::Result instance. def select_all(arel, name = nil, binds = []) arel, binds = binds_from_relation arel, binds - select(to_sql(arel, binds), name, binds) + sql = to_sql(arel, binds) + if arel.is_a?(String) + preparable = false + else + preparable = visitor.preparable + end + if prepared_statements && preparable + select_prepared(sql, name, binds) + else + select(sql, name, binds) + end end # Returns a record hash with the column names as keys and column values @@ -67,7 +77,7 @@ module ActiveRecord # Executes +sql+ statement in the context of this connection using # +binds+ as the bind substitutes. +name+ is logged along with # the executed +sql+ statement. - def exec_query(sql, name = 'SQL', binds = []) + def exec_query(sql, name = 'SQL', binds = [], prepare: false) end # Executes insert +sql+ statement in the context of this connection using @@ -192,7 +202,7 @@ module ActiveRecord # * http://www.postgresql.org/docs/current/static/transaction-iso.html # * https://dev.mysql.com/doc/refman/5.7/en/set-transaction.html # - # An <tt>ActiveRecord::TransactionIsolationError</tt> will be raised if: + # An ActiveRecord::TransactionIsolationError will be raised if: # # * The adapter does not support setting the isolation level # * You are joining an existing open transaction @@ -358,9 +368,12 @@ module ActiveRecord # Returns an ActiveRecord::Result instance. def select(sql, name = nil, binds = []) - exec_query(sql, name, binds) + exec_query(sql, name, binds, prepare: false) end + def select_prepared(sql, name = nil, binds = []) + exec_query(sql, name, binds, prepare: true) + end # Returns the last auto-generated ID from the affected table. def insert_sql(sql, name = nil, pk = nil, id_value = nil, sequence_name = nil) diff --git a/activerecord/lib/active_record/connection_adapters/abstract/quoting.rb b/activerecord/lib/active_record/connection_adapters/abstract/quoting.rb index 2c7409b2dc..9ec0a67c8f 100644 --- a/activerecord/lib/active_record/connection_adapters/abstract/quoting.rb +++ b/activerecord/lib/active_record/connection_adapters/abstract/quoting.rb @@ -43,9 +43,9 @@ module ActiveRecord # If you are having to call this function, you are likely doing something # wrong. The column does not have sufficient type information if the user # provided a custom type on the class level either explicitly (via - # `attribute`) or implicitly (via `serialize`, - # `time_zone_aware_attributes`). In almost all cases, the sql type should - # only be used to change quoting behavior, when the primitive to + # Attributes::ClassMethods#attribute) or implicitly (via + # AttributeMethods::Serialization::ClassMethods#serialize, +time_zone_aware_attributes+). + # In almost all cases, the sql type should only be used to change quoting behavior, when the primitive to # represent the type doesn't sufficiently reflect the differences # (varchar vs binary) for example. The type used to get this primitive # should have been provided before reaching the connection adapter. @@ -58,7 +58,7 @@ module ActiveRecord end end - # See docs for +type_cast_from_column+ + # See docs for #type_cast_from_column def lookup_cast_type_from_column(column) # :nodoc: lookup_cast_type(column.sql_type) 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 10329de5f4..abf0124562 100644 --- a/activerecord/lib/active_record/connection_adapters/abstract/schema_definitions.rb +++ b/activerecord/lib/active_record/connection_adapters/abstract/schema_definitions.rb @@ -123,23 +123,29 @@ module ActiveRecord end def foreign_key_options - as_options(foreign_key) + as_options(foreign_key).merge(column: column_name) end def columns - result = [["#{name}_id", type, options]] + result = [[column_name, type, options]] if polymorphic result.unshift(["#{name}_type", :string, polymorphic_options]) end result end + def column_name + "#{name}_id" + end + def column_names columns.map(&:first) end def foreign_table_name - Base.pluralize_table_names ? name.to_s.pluralize : name + foreign_key_options.fetch(:to_table) do + Base.pluralize_table_names ? name.to_s.pluralize : name + end end end @@ -181,7 +187,7 @@ module ActiveRecord # Represents the schema of an SQL table in an abstract way. This class # provides methods for manipulating the schema representation. # - # Inside migration files, the +t+ object in +create_table+ + # Inside migration files, the +t+ object in {create_table}[rdoc-ref:SchemaStatements#create_table] # is actually of this type: # # class SomeMigration < ActiveRecord::Migration @@ -196,15 +202,11 @@ module ActiveRecord # end # end # - # The table definitions - # The Columns are stored as a ColumnDefinition in the +columns+ attribute. class TableDefinition include ColumnMethods - # An array of ColumnDefinition objects, representing the column changes - # that have been defined. attr_accessor :indexes - attr_reader :name, :temporary, :options, :as, :foreign_keys + attr_reader :name, :temporary, :options, :as, :foreign_keys, :native def initialize(types, name, temporary, options, as = nil) @columns_hash = {} @@ -232,90 +234,23 @@ module ActiveRecord end # Instantiates a new column for the table. - # The +type+ parameter is normally one of the migrations native types, - # which is one of the following: - # <tt>:primary_key</tt>, <tt>:string</tt>, <tt>:text</tt>, - # <tt>:integer</tt>, <tt>:bigint</tt>, <tt>:float</tt>, <tt>:decimal</tt>, - # <tt>:datetime</tt>, <tt>:time</tt>, <tt>:date</tt>, - # <tt>:binary</tt>, <tt>:boolean</tt>. - # - # You may use a type not in this list as long as it is supported by your - # database (for example, "polygon" in MySQL), but this will not be database - # agnostic and should usually be avoided. - # - # Available options are (none of these exists by default): - # * <tt>:limit</tt> - - # Requests a maximum column length. This is number of characters for a <tt>:string</tt> column - # and number of bytes for <tt>:text</tt>, <tt>:binary</tt> and <tt>:integer</tt> columns. - # * <tt>:default</tt> - - # The column's default value. Use nil for NULL. - # * <tt>:null</tt> - - # Allows or disallows +NULL+ values in the column. This option could - # have been named <tt>:null_allowed</tt>. - # * <tt>:precision</tt> - - # Specifies the precision for a <tt>:decimal</tt> column. - # * <tt>:scale</tt> - - # Specifies the scale for a <tt>:decimal</tt> column. + # See {connection.add_column}[rdoc-ref:ConnectionAdapters::SchemaStatements#add_column] + # for available options. + # + # Additional options are: # * <tt>:index</tt> - # Create an index for the column. Can be either <tt>true</tt> or an options hash. # - # Note: The precision is the total number of significant digits - # and the scale is the number of digits that can be stored following - # the decimal point. For example, the number 123.45 has a precision of 5 - # and a scale of 2. A decimal with a precision of 5 and a scale of 2 can - # range from -999.99 to 999.99. - # - # Please be aware of different RDBMS implementations behavior with - # <tt>:decimal</tt> columns: - # * The SQL standard says the default scale should be 0, <tt>:scale</tt> <= - # <tt>:precision</tt>, and makes no comments about the requirements of - # <tt>:precision</tt>. - # * MySQL: <tt>:precision</tt> [1..63], <tt>:scale</tt> [0..30]. - # Default is (10,0). - # * PostgreSQL: <tt>:precision</tt> [1..infinity], - # <tt>:scale</tt> [0..infinity]. No default. - # * SQLite2: Any <tt>:precision</tt> and <tt>:scale</tt> may be used. - # Internal storage as strings. No default. - # * SQLite3: No restrictions on <tt>:precision</tt> and <tt>:scale</tt>, - # but the maximum supported <tt>:precision</tt> is 16. No default. - # * Oracle: <tt>:precision</tt> [1..38], <tt>:scale</tt> [-84..127]. - # Default is (38,0). - # * DB2: <tt>:precision</tt> [1..63], <tt>:scale</tt> [0..62]. - # Default unknown. - # * SqlServer?: <tt>:precision</tt> [1..38], <tt>:scale</tt> [0..38]. - # Default (38,0). - # # This method returns <tt>self</tt>. # # == Examples - # # Assuming +td+ is an instance of TableDefinition - # td.column(:granted, :boolean) - # # granted BOOLEAN - # - # td.column(:picture, :binary, limit: 2.megabytes) - # # => picture BLOB(2097152) # - # td.column(:sales_stage, :string, limit: 20, default: 'new', null: false) - # # => sales_stage VARCHAR(20) DEFAULT 'new' NOT NULL - # - # td.column(:bill_gates_money, :decimal, precision: 15, scale: 2) - # # => bill_gates_money DECIMAL(15,2) - # - # td.column(:sensor_reading, :decimal, precision: 30, scale: 20) - # # => sensor_reading DECIMAL(30,20) - # - # # While <tt>:scale</tt> defaults to zero on most databases, it - # # probably wouldn't hurt to include it. - # td.column(:huge_integer, :decimal, precision: 30) - # # => huge_integer DECIMAL(30) - # - # # Defines a column with a database-specific type. - # td.column(:foo, 'polygon') - # # => foo polygon + # # Assuming +td+ is an instance of TableDefinition + # td.column(:granted, :boolean, index: true) # # == Short-hand examples # - # Instead of calling +column+ directly, you can also work with the short-hand definitions for the default types. + # Instead of calling #column directly, you can also work with the short-hand definitions for the default types. # They use the type as the method name instead of as a parameter and allow for multiple columns to be defined # in a single statement. # @@ -347,7 +282,8 @@ 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. The <tt>:index</tt> option - # will also create an index, similar to calling <tt>add_index</tt>. So what can be written like this: + # will also create an index, similar to calling {add_index}[rdoc-ref:ConnectionAdapters::SchemaStatements#add_index]. + # So what can be written like this: # # create_table :taggings do |t| # t.integer :tag_id, :tagger_id, :taggable_id @@ -398,7 +334,7 @@ module ActiveRecord end # Appends <tt>:datetime</tt> columns <tt>:created_at</tt> and - # <tt>:updated_at</tt> to the table. See SchemaStatements#add_timestamps + # <tt>:updated_at</tt> to the table. See {connection.add_timestamps}[rdoc-ref:SchemaStatements#add_timestamps] # # t.timestamps null: false def timestamps(*args) @@ -415,7 +351,7 @@ module ActiveRecord # t.references(:user) # t.belongs_to(:supplier, foreign_key: true) # - # See SchemaStatements#add_reference for details of the options you can use. + # See {connection.add_reference}[rdoc-ref:SchemaStatements#add_reference] for details of the options you can use. def references(*args, **options) args.each do |col| ReferenceDefinition.new(col, **options).add_to(self) @@ -448,10 +384,6 @@ module ActiveRecord ColumnDefinition.new name, type end - def native - @native - end - def aliased_types(name, fallback) 'timestamp' == name ? :datetime : fallback end @@ -487,7 +419,7 @@ module ActiveRecord end # Represents an SQL table in an abstract way for updating a table. - # Also see TableDefinition and SchemaStatements#create_table + # Also see TableDefinition and {connection.create_table}[rdoc-ref:SchemaStatements#create_table] # # Available transformations are: # @@ -544,7 +476,7 @@ module ActiveRecord # # t.string(:name) unless t.column_exists?(:name, :string) # - # See SchemaStatements#column_exists? + # See {connection.column_exists?}[rdoc-ref:SchemaStatements#column_exists?] def column_exists?(column_name, type = nil, options = {}) @base.column_exists?(name, column_name, type, options) end @@ -556,7 +488,7 @@ module ActiveRecord # t.index([:branch_id, :party_id], unique: true) # t.index([:branch_id, :party_id], unique: true, name: 'by_branch_party') # - # See SchemaStatements#add_index for details of the options you can use. + # See {connection.add_index}[rdoc-ref:SchemaStatements#add_index] for details of the options you can use. def index(column_name, options = {}) @base.add_index(name, column_name, options) end @@ -567,7 +499,7 @@ module ActiveRecord # t.index(:branch_id) # end # - # See SchemaStatements#index_exists? + # See {connection.index_exists?}[rdoc-ref:SchemaStatements#index_exists?] def index_exists?(column_name, options = {}) @base.index_exists?(name, column_name, options) end @@ -576,7 +508,7 @@ module ActiveRecord # # t.rename_index(:user_id, :account_id) # - # See SchemaStatements#rename_index + # See {connection.rename_index}[rdoc-ref:SchemaStatements#rename_index] def rename_index(index_name, new_index_name) @base.rename_index(name, index_name, new_index_name) end @@ -585,7 +517,7 @@ module ActiveRecord # # t.timestamps(null: false) # - # See SchemaStatements#add_timestamps + # See {connection.add_timestamps}[rdoc-ref:SchemaStatements#add_timestamps] def timestamps(options = {}) @base.add_timestamps(name, options) end @@ -606,7 +538,7 @@ module ActiveRecord # t.change_default(:authorized, 1) # t.change_default(:status, from: nil, to: "draft") # - # See SchemaStatements#change_column_default + # See {connection.change_column_default}[rdoc-ref:SchemaStatements#change_column_default] def change_default(column_name, default_or_changes) @base.change_column_default(name, column_name, default_or_changes) end @@ -616,7 +548,7 @@ module ActiveRecord # t.remove(:qualification) # t.remove(:qualification, :experience) # - # See SchemaStatements#remove_columns + # See {connection.remove_columns}[rdoc-ref:SchemaStatements#remove_columns] def remove(*column_names) @base.remove_columns(name, *column_names) end @@ -627,7 +559,7 @@ module ActiveRecord # t.remove_index(column: [:branch_id, :party_id]) # t.remove_index(name: :by_branch_party) # - # See SchemaStatements#remove_index + # See {connection.remove_index}[rdoc-ref:SchemaStatements#remove_index] def remove_index(options = {}) @base.remove_index(name, options) end @@ -636,7 +568,7 @@ module ActiveRecord # # t.remove_timestamps # - # See SchemaStatements#remove_timestamps + # See {connection.remove_timestamps}[rdoc-ref:SchemaStatements#remove_timestamps] def remove_timestamps(options = {}) @base.remove_timestamps(name, options) end @@ -645,7 +577,7 @@ module ActiveRecord # # t.rename(:description, :name) # - # See SchemaStatements#rename_column + # See {connection.rename_column}[rdoc-ref:SchemaStatements#rename_column] def rename(column_name, new_column_name) @base.rename_column(name, column_name, new_column_name) end @@ -655,7 +587,7 @@ module ActiveRecord # t.references(:user) # t.belongs_to(:supplier, foreign_key: true) # - # See SchemaStatements#add_reference for details of the options you can use. + # See {connection.add_reference}[rdoc-ref:SchemaStatements#add_reference] for details of the options you can use. def references(*args) options = args.extract_options! args.each do |ref_name| @@ -669,7 +601,7 @@ module ActiveRecord # t.remove_references(:user) # t.remove_belongs_to(:supplier, polymorphic: true) # - # See SchemaStatements#remove_reference + # See {connection.remove_reference}[rdoc-ref:SchemaStatements#remove_reference] def remove_references(*args) options = args.extract_options! args.each do |ref_name| @@ -682,7 +614,7 @@ module ActiveRecord # # t.foreign_key(:authors) # - # See SchemaStatements#add_foreign_key + # See {connection.add_foreign_key}[rdoc-ref:SchemaStatements#add_foreign_key] def foreign_key(*args) # :nodoc: @base.add_foreign_key(name, *args) end @@ -691,7 +623,7 @@ module ActiveRecord # # t.foreign_key(:authors) unless t.foreign_key_exists?(:authors) # - # See SchemaStatements#foreign_key_exists? + # See {connection.foreign_key_exists?}[rdoc-ref:SchemaStatements#foreign_key_exists?] def foreign_key_exists?(*args) # :nodoc: @base.foreign_key_exists?(name, *args) end diff --git a/activerecord/lib/active_record/connection_adapters/abstract/schema_dumper.rb b/activerecord/lib/active_record/connection_adapters/abstract/schema_dumper.rb index a2f58364a4..e252ddb4cf 100644 --- a/activerecord/lib/active_record/connection_adapters/abstract/schema_dumper.rb +++ b/activerecord/lib/active_record/connection_adapters/abstract/schema_dumper.rb @@ -20,7 +20,7 @@ module ActiveRecord # This can be overridden on an Adapter level basis to support other # extended datatypes (Example: Adding an array option in the - # PostgreSQLAdapter) + # PostgreSQL::ColumnDumper) def prepare_column_options(column) spec = {} spec[:name] = column.name.inspect 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 7e53f8958a..d5f8dbc8fc 100644 --- a/activerecord/lib/active_record/connection_adapters/abstract/schema_statements.rb +++ b/activerecord/lib/active_record/connection_adapters/abstract/schema_statements.rb @@ -129,7 +129,7 @@ module ActiveRecord # Creates a new table with the name +table_name+. +table_name+ may either # be a String or a Symbol. # - # There are two ways to work with +create_table+. You can use the block + # There are two ways to work with #create_table. You can use the block # form or the regular form, like this: # # === Block form @@ -161,7 +161,7 @@ module ActiveRecord # The +options+ hash can include the following keys: # [<tt>:id</tt>] # Whether to automatically add a primary key column. Defaults to true. - # Join tables for +has_and_belongs_to_many+ should set it to false. + # Join tables for {ActiveRecord::Base.has_and_belongs_to_many}[rdoc-ref:Associations::ClassMethods#has_and_belongs_to_many] should set it to false. # # A Symbol can be used to specify the type of the generated primary key column. # [<tt>:primary_key</tt>] @@ -169,7 +169,8 @@ module ActiveRecord # Defaults to +id+. If <tt>:id</tt> is false this option is ignored. # # Note that Active Record models will automatically detect their - # primary key. This can be avoided by using +self.primary_key=+ on the model + # primary key. This can be avoided by using + # {self.primary_key=}[rdoc-ref:AttributeMethods::PrimaryKey::ClassMethods#primary_key=] on the model # to define the key explicitly. # # [<tt>:options</tt>] @@ -296,7 +297,7 @@ module ActiveRecord # Set to true to drop the table before creating it. # Defaults to false. # - # Note that +create_join_table+ does not create any indices by default; you can use + # Note that #create_join_table does not create any indices by default; you can use # its block form to do so yourself: # # create_join_table :products, :categories do |t| @@ -331,11 +332,11 @@ module ActiveRecord end # Drops the join table specified by the given arguments. - # See +create_join_table+ for details. + # See #create_join_table for details. # # Although this command ignores the block if one is given, it can be helpful # to provide one in a migration's +change+ method so it can be reverted. - # In that case, the block will be used by create_join_table. + # In that case, the block will be used by #create_join_table. def drop_join_table(table_1, table_2, options = {}) join_table_name = find_join_table_name(table_1, table_2, options) drop_table(join_table_name) @@ -440,17 +441,86 @@ module ActiveRecord # # Although this command ignores most +options+ and the block if one is given, # it can be helpful to provide these in a migration's +change+ method so it can be reverted. - # In that case, +options+ and the block will be used by create_table. + # In that case, +options+ and the block will be used by #create_table. def drop_table(table_name, options = {}) execute "DROP TABLE#{' IF EXISTS' if options[:if_exists]} #{quote_table_name(table_name)}" end - # Adds a new column to the named table. - # See TableDefinition#column for details of the options you can use. - # - # Note: Not all options will be available, generally this command should - # ignore most of them. In favor of doing a low-level call to simply - # create a column. + # Add a new +type+ column named +column_name+ to +table_name+. + # + # The +type+ parameter is normally one of the migrations native types, + # which is one of the following: + # <tt>:primary_key</tt>, <tt>:string</tt>, <tt>:text</tt>, + # <tt>:integer</tt>, <tt>:bigint</tt>, <tt>:float</tt>, <tt>:decimal</tt>, + # <tt>:datetime</tt>, <tt>:time</tt>, <tt>:date</tt>, + # <tt>:binary</tt>, <tt>:boolean</tt>. + # + # You may use a type not in this list as long as it is supported by your + # database (for example, "polygon" in MySQL), but this will not be database + # agnostic and should usually be avoided. + # + # Available options are (none of these exists by default): + # * <tt>:limit</tt> - + # Requests a maximum column length. This is number of characters for a <tt>:string</tt> column + # and number of bytes for <tt>:text</tt>, <tt>:binary</tt> and <tt>:integer</tt> columns. + # * <tt>:default</tt> - + # The column's default value. Use nil for NULL. + # * <tt>:null</tt> - + # Allows or disallows +NULL+ values in the column. This option could + # have been named <tt>:null_allowed</tt>. + # * <tt>:precision</tt> - + # Specifies the precision for a <tt>:decimal</tt> column. + # * <tt>:scale</tt> - + # Specifies the scale for a <tt>:decimal</tt> column. + # + # Note: The precision is the total number of significant digits + # and the scale is the number of digits that can be stored following + # the decimal point. For example, the number 123.45 has a precision of 5 + # and a scale of 2. A decimal with a precision of 5 and a scale of 2 can + # range from -999.99 to 999.99. + # + # Please be aware of different RDBMS implementations behavior with + # <tt>:decimal</tt> columns: + # * The SQL standard says the default scale should be 0, <tt>:scale</tt> <= + # <tt>:precision</tt>, and makes no comments about the requirements of + # <tt>:precision</tt>. + # * MySQL: <tt>:precision</tt> [1..63], <tt>:scale</tt> [0..30]. + # Default is (10,0). + # * PostgreSQL: <tt>:precision</tt> [1..infinity], + # <tt>:scale</tt> [0..infinity]. No default. + # * SQLite2: Any <tt>:precision</tt> and <tt>:scale</tt> may be used. + # Internal storage as strings. No default. + # * SQLite3: No restrictions on <tt>:precision</tt> and <tt>:scale</tt>, + # but the maximum supported <tt>:precision</tt> is 16. No default. + # * Oracle: <tt>:precision</tt> [1..38], <tt>:scale</tt> [-84..127]. + # Default is (38,0). + # * DB2: <tt>:precision</tt> [1..63], <tt>:scale</tt> [0..62]. + # Default unknown. + # * SqlServer?: <tt>:precision</tt> [1..38], <tt>:scale</tt> [0..38]. + # Default (38,0). + # + # == Examples + # + # add_column(:users, :picture, :binary, limit: 2.megabytes) + # # ALTER TABLE "users" ADD "picture" blob(2097152) + # + # add_column(:articles, :status, :string, limit: 20, default: 'draft', null: false) + # # ALTER TABLE "articles" ADD "status" varchar(20) DEFAULT 'draft' NOT NULL + # + # add_column(:answers, :bill_gates_money, :decimal, precision: 15, scale: 2) + # # ALTER TABLE "answers" ADD "bill_gates_money" decimal(15,2) + # + # add_column(:measurements, :sensor_reading, :decimal, precision: 30, scale: 20) + # # ALTER TABLE "measurements" ADD "sensor_reading" decimal(30,20) + # + # # While :scale defaults to zero on most databases, it + # # probably wouldn't hurt to include it. + # add_column(:measurements, :huge_integer, :decimal, precision: 30) + # # ALTER TABLE "measurements" ADD "huge_integer" decimal(30) + # + # # Defines a column with a database-specific type. + # add_column(:shapes, :triangle, 'polygon') + # # ALTER TABLE "shapes" ADD "triangle" polygon def add_column(table_name, column_name, type, options = {}) at = create_alter_table table_name at.add_column(column_name, type, options) @@ -694,7 +764,7 @@ module ActiveRecord # Adds a reference. The reference column is an integer by default, # the <tt>:type</tt> option can be used to specify a different type. # Optionally adds a +_type+ column, if <tt>:polymorphic</tt> option is provided. - # <tt>add_reference</tt> and <tt>add_belongs_to</tt> are acceptable. + # #add_reference and #add_belongs_to are acceptable. # # The +options+ hash can include the following keys: # [<tt>:type</tt>] @@ -702,9 +772,11 @@ module ActiveRecord # [<tt>:index</tt>] # Add an appropriate index. Defaults to false. # [<tt>:foreign_key</tt>] - # Add an appropriate foreign key. Defaults to false. + # Add an appropriate foreign key constraint. Defaults to false. # [<tt>:polymorphic</tt>] # Whether an additional +_type+ column should be added. Defaults to false. + # [<tt>:null</tt>] + # Whether the column allows nulls. Defaults to true. # # ====== Create a user_id integer column # @@ -722,13 +794,17 @@ module ActiveRecord # # add_reference(:products, :supplier, foreign_key: true) # + # ====== Create a supplier_id column and a foreign key to the firms table + # + # add_reference(:products, :supplier, foreign_key: {to_table: :firms}) + # def add_reference(table_name, *args) ReferenceDefinition.new(*args).add_to(update_table_definition(table_name, self)) end alias :add_belongs_to :add_reference # Removes the reference(s). Also removes a +type+ column if one exists. - # <tt>remove_reference</tt> and <tt>remove_belongs_to</tt> are acceptable. + # #remove_reference and #remove_belongs_to are acceptable. # # ====== Remove the reference # @@ -754,7 +830,7 @@ module ActiveRecord alias :remove_belongs_to :remove_reference # Returns an array of foreign keys for the given table. - # The foreign keys are represented as +ForeignKeyDefinition+ objects. + # The foreign keys are represented as ForeignKeyDefinition objects. def foreign_keys(table_name) raise NotImplementedError, "foreign_keys is not implemented" end diff --git a/activerecord/lib/active_record/connection_adapters/abstract_adapter.rb b/activerecord/lib/active_record/connection_adapters/abstract_adapter.rb index ed14c781c6..55910865e5 100644 --- a/activerecord/lib/active_record/connection_adapters/abstract_adapter.rb +++ b/activerecord/lib/active_record/connection_adapters/abstract_adapter.rb @@ -1,5 +1,6 @@ require 'active_record/type' require 'active_support/core_ext/benchmark' +require 'active_record/connection_adapters/determine_if_preparable_visitor' require 'active_record/connection_adapters/schema_cache' require 'active_record/connection_adapters/sql_type_metadata' require 'active_record/connection_adapters/abstract/schema_dumper' @@ -51,15 +52,15 @@ module ActiveRecord # related classes form the abstraction layer which makes this possible. # An AbstractAdapter represents a connection to a database, and provides an # abstract interface for database-specific functionality such as establishing - # a connection, escaping values, building the right SQL fragments for ':offset' - # and ':limit' options, etc. + # a connection, escaping values, building the right SQL fragments for +:offset+ + # and +:limit+ options, etc. # # All the concrete database adapters follow the interface laid down in this class. - # ActiveRecord::Base.connection returns an AbstractAdapter object, which + # {ActiveRecord::Base.connection}[rdoc-ref:ConnectionHandling#connection] returns an AbstractAdapter object, which # you can use. # # Most of the methods in the adapter are useful during migrations. Most - # notably, the instance methods provided by SchemaStatement are very useful. + # notably, the instance methods provided by SchemaStatements are very useful. class AbstractAdapter ADAPTER_NAME = 'Abstract'.freeze include Quoting, DatabaseStatements, SchemaStatements @@ -213,6 +214,11 @@ module ActiveRecord false end + # Does this adapter support application-enforced advisory locking? + def supports_advisory_locks? + false + end + # Should primary key values be selected from their corresponding # sequence before the insert statement? If true, next_sequence_value # is called before each insert to set the record's primary key. @@ -279,6 +285,20 @@ module ActiveRecord def enable_extension(name) end + # This is meant to be implemented by the adapters that support advisory + # locks + # + # Return true if we got the lock, otherwise false + def get_advisory_lock(key) # :nodoc: + end + + # This is meant to be implemented by the adapters that support advisory + # locks. + # + # Return true if we released the lock, otherwise false + def release_advisory_lock(key) # :nodoc: + end + # A list of extensions, to be filled in by adapters that support them. def extensions [] @@ -348,7 +368,7 @@ module ActiveRecord end # Checks whether the connection to the database is still active (i.e. not stale). - # This is done under the hood by calling <tt>active?</tt>. If the connection + # This is done under the hood by calling #active?. If the connection # is no longer active, then this method will reconnect to the database. def verify!(*ignored) reconnect! unless active? @@ -518,7 +538,7 @@ module ActiveRecord def translate_exception(exception, message) # override in derived class - ActiveRecord::StatementInvalid.new(message, exception) + ActiveRecord::StatementInvalid.new(message) end def without_prepared_statement?(binds) 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 79a24d1996..3e3bbc267b 100644 --- a/activerecord/lib/active_record/connection_adapters/abstract_mysql_adapter.rb +++ b/activerecord/lib/active_record/connection_adapters/abstract_mysql_adapter.rb @@ -1,173 +1,24 @@ +require 'active_record/connection_adapters/abstract_adapter' +require 'active_record/connection_adapters/mysql/schema_creation' +require 'active_record/connection_adapters/mysql/schema_definitions' +require 'active_record/connection_adapters/mysql/schema_dumper' + require 'active_support/core_ext/string/strip' module ActiveRecord module ConnectionAdapters class AbstractMysqlAdapter < AbstractAdapter + include MySQL::ColumnDumper include Savepoints - module ColumnMethods - def primary_key(name, type = :primary_key, **options) - options[:auto_increment] = true if type == :bigint - super - end - - def json(*args, **options) - args.each { |name| column(name, :json, options) } - end - - def unsigned_integer(*args, **options) - args.each { |name| column(name, :unsigned_integer, options) } - end - - def unsigned_bigint(*args, **options) - args.each { |name| column(name, :unsigned_bigint, options) } - end - - def unsigned_float(*args, **options) - args.each { |name| column(name, :unsigned_float, options) } - end - - def unsigned_decimal(*args, **options) - args.each { |name| column(name, :unsigned_decimal, options) } - end - end - - class ColumnDefinition < ActiveRecord::ConnectionAdapters::ColumnDefinition - attr_accessor :charset, :unsigned - end - - class TableDefinition < ActiveRecord::ConnectionAdapters::TableDefinition - include ColumnMethods - - def new_column_definition(name, type, options) # :nodoc: - column = super - case column.type - when :primary_key - column.type = :integer - column.auto_increment = true - when /\Aunsigned_(?<type>.+)\z/ - column.type = $~[:type].to_sym - column.unsigned = true - end - column.unsigned ||= options[:unsigned] - column.charset = options[:charset] - column - end - - private - - def create_column_definition(name, type) - ColumnDefinition.new(name, type) - end - end - - class Table < ActiveRecord::ConnectionAdapters::Table - include ColumnMethods - end - - class SchemaCreation < AbstractAdapter::SchemaCreation - private - - def visit_DropForeignKey(name) - "DROP FOREIGN KEY #{name}" - end - - def visit_ColumnDefinition(o) - o.sql_type = type_to_sql(o.type, o.limit, o.precision, o.scale, o.unsigned) - super - end - - def visit_AddColumnDefinition(o) - add_column_position!(super, column_options(o.column)) - end - - def visit_ChangeColumnDefinition(o) - change_column_sql = "CHANGE #{quote_column_name(o.name)} #{accept(o.column)}" - add_column_position!(change_column_sql, column_options(o.column)) - end - - def column_options(o) - column_options = super - column_options[:charset] = o.charset - column_options - end - - def add_column_options!(sql, options) - if options[:charset] - sql << " CHARACTER SET #{options[:charset]}" - end - if options[:collation] - sql << " COLLATE #{options[:collation]}" - end - super - end - - def add_column_position!(sql, options) - if options[:first] - sql << " FIRST" - elsif options[:after] - sql << " AFTER #{quote_column_name(options[:after])}" - end - sql - end - - def index_in_create(table_name, column_name, options) - index_name, index_type, index_columns, _, _, index_using = @conn.add_index_options(table_name, column_name, options) - "#{index_type} INDEX #{quote_column_name(index_name)} #{index_using} (#{index_columns}) " - end - end - def update_table_definition(table_name, base) # :nodoc: - Table.new(table_name, base) + MySQL::Table.new(table_name, base) end def schema_creation - SchemaCreation.new self - end - - def column_spec_for_primary_key(column) - spec = {} - if column.auto_increment? - spec[:id] = ':bigint' if column.bigint? - spec[:unsigned] = 'true' if column.unsigned? - return if spec.empty? - else - spec[:id] = column.type.inspect - spec.merge!(prepare_column_options(column).delete_if { |key, _| [:name, :type, :null].include?(key) }) - end - spec - end - - def prepare_column_options(column) - spec = super - spec[:unsigned] = 'true' if column.unsigned? - spec - end - - def migration_keys - super + [:unsigned] - end - - private - - def schema_limit(column) - super unless column.type == :boolean + MySQL::SchemaCreation.new(self) end - def schema_precision(column) - super unless /time/ === column.sql_type && column.precision == 0 - end - - def schema_collation(column) - if column.collation && table_name = column.instance_variable_get(:@table_name) - @collation_cache ||= {} - @collation_cache[table_name] ||= select_one("SHOW TABLE STATUS LIKE '#{table_name}'")["Collation"] - column.collation.inspect if column.collation != @collation_cache[table_name] - end - end - - public - class Column < ConnectionAdapters::Column # :nodoc: delegate :strict, :extra, to: :sql_type_metadata, allow_nil: true @@ -283,6 +134,7 @@ module ActiveRecord time: { name: "time" }, date: { name: "date" }, binary: { name: "blob" }, + blob: { name: "blob" }, boolean: { name: "tinyint", limit: 1 }, bigint: { name: "bigint" }, json: { name: "json" }, @@ -301,6 +153,7 @@ module ActiveRecord if self.class.type_cast_config_to_boolean(config.fetch(:prepared_statements) { true }) @prepared_statements = true + @visitor.extend(DetermineIfPreparableVisitor) else @prepared_statements = false end @@ -316,6 +169,10 @@ module ActiveRecord end end + def version + @version ||= Version.new(full_version.match(/^\d+\.\d+\.\d+/)[0]) + end + # Returns true, since this connection adapter supports migrations. def supports_migrations? true @@ -363,6 +220,20 @@ module ActiveRecord version >= '5.6.4' end + # 5.0.0 definitely supports it, possibly supported by earlier versions but + # not sure + def supports_advisory_locks? + version >= '5.0.0' + end + + def get_advisory_lock(key, timeout = 0) # :nodoc: + select_value("SELECT GET_LOCK('#{key}', #{timeout});").to_s == '1' + end + + def release_advisory_lock(key) # :nodoc: + select_value("SELECT RELEASE_LOCK('#{key}')").to_s == '1' + end + def native_database_types NATIVE_DATABASE_TYPES end @@ -458,7 +329,7 @@ module ActiveRecord end class ExplainPrettyPrinter # :nodoc: - # Pretty prints the result of a EXPLAIN in a way that resembles the output of the + # Pretty prints the result of an EXPLAIN in a way that resembles the output of the # MySQL shell: # # +----+-------------+-------+-------+---------------+---------+---------+-------+------+-------------+ @@ -626,7 +497,10 @@ module ActiveRecord end def tables(name = nil) # :nodoc: - select_values("SHOW FULL TABLES", 'SCHEMA') + sql = "SELECT table_name FROM information_schema.tables " + sql << "WHERE table_schema = #{quote(@config[:database])}" + + select_values(sql, 'SCHEMA') end alias data_sources tables @@ -828,12 +702,18 @@ module ActiveRecord # Maps logical Rails types to MySQL-specific data types. def type_to_sql(type, limit = nil, precision = nil, scale = nil, unsigned = nil) sql = case type.to_s - when 'binary' - binary_to_sql(limit) when 'integer' integer_to_sql(limit) when 'text' text_to_sql(limit) + when 'blob' + binary_to_sql(limit) + when 'binary' + if (0..0xfff) === limit + "varbinary(#{limit})" + else + binary_to_sql(limit) + end else super(type, limit, precision, scale) end @@ -850,13 +730,6 @@ module ActiveRecord nil end - # Returns a table's primary key and belonging sequence. - def pk_and_sequence_for(table) - if pk = primary_key(table) - [ pk, nil ] - end - end - def primary_keys(table_name) # :nodoc: raise ArgumentError unless table_name.present? @@ -994,9 +867,9 @@ module ActiveRecord def translate_exception(exception, message) case error_number(exception) when 1062 - RecordNotUnique.new(message, exception) + RecordNotUnique.new(message) when 1452 - InvalidForeignKey.new(message, exception) + InvalidForeignKey.new(message) else super end @@ -1080,10 +953,6 @@ module ActiveRecord subselect.from subsubselect.distinct.as('__active_record_temp') end - def version - @version ||= Version.new(full_version.match(/^\d+\.\d+\.\d+/)[0]) - end - def mariadb? full_version =~ /mariadb/i end @@ -1150,16 +1019,7 @@ module ActiveRecord end def create_table_definition(name, temporary = false, options = nil, as = nil) # :nodoc: - TableDefinition.new(native_database_types, name, temporary, options, as) - end - - def binary_to_sql(limit) # :nodoc: - case limit - when 0..0xfff; "varbinary(#{limit})" - when nil; "blob" - when 0x1000..0xffffffff; "blob(#{limit})" - else raise(ActiveRecordError, "No binary type has byte length #{limit}") - end + MySQL::TableDefinition.new(native_database_types, name, temporary, options, as) end def integer_to_sql(limit) # :nodoc: @@ -1184,6 +1044,16 @@ module ActiveRecord end end + def binary_to_sql(limit) # :nodoc: + case limit + when 0..0xff; 'tinyblob' + when nil, 0x100..0xffff; 'blob' + when 0x10000..0xffffff; 'mediumblob' + when 0x1000000..0xffffffff; 'longblob' + else raise(ActiveRecordError, "No binary type has byte length #{limit}") + end + end + class MysqlJson < Type::Internal::AbstractJson # :nodoc: def changed_in_place?(raw_old_value, new_value) # Normalization is required because MySQL JSON data format includes diff --git a/activerecord/lib/active_record/connection_adapters/column.rb b/activerecord/lib/active_record/connection_adapters/column.rb index 5e31efec4a..81de7c03fb 100644 --- a/activerecord/lib/active_record/connection_adapters/column.rb +++ b/activerecord/lib/active_record/connection_adapters/column.rb @@ -16,7 +16,7 @@ module ActiveRecord # +sql_type_metadata+ is various information about the type of the column # +null+ determines if this column allows +NULL+ values. def initialize(name, default, sql_type_metadata = nil, null = true, default_function = nil, collation = nil) - @name = name + @name = name.freeze @sql_type_metadata = sql_type_metadata @null = null @default = default diff --git a/activerecord/lib/active_record/connection_adapters/determine_if_preparable_visitor.rb b/activerecord/lib/active_record/connection_adapters/determine_if_preparable_visitor.rb new file mode 100644 index 0000000000..0fdc185c45 --- /dev/null +++ b/activerecord/lib/active_record/connection_adapters/determine_if_preparable_visitor.rb @@ -0,0 +1,22 @@ +module ActiveRecord + module ConnectionAdapters + module DetermineIfPreparableVisitor + attr_reader :preparable + + def accept(*) + @preparable = true + super + end + + def visit_Arel_Nodes_In(*) + @preparable = false + super + end + + def visit_Arel_Nodes_SqlLiteral(*) + @preparable = false + super + end + end + end +end diff --git a/activerecord/lib/active_record/connection_adapters/mysql/schema_creation.rb b/activerecord/lib/active_record/connection_adapters/mysql/schema_creation.rb new file mode 100644 index 0000000000..1e2c859af9 --- /dev/null +++ b/activerecord/lib/active_record/connection_adapters/mysql/schema_creation.rb @@ -0,0 +1,57 @@ +module ActiveRecord + module ConnectionAdapters + module MySQL + class SchemaCreation < AbstractAdapter::SchemaCreation + private + + def visit_DropForeignKey(name) + "DROP FOREIGN KEY #{name}" + end + + def visit_ColumnDefinition(o) + o.sql_type = type_to_sql(o.type, o.limit, o.precision, o.scale, o.unsigned) + super + end + + def visit_AddColumnDefinition(o) + add_column_position!(super, column_options(o.column)) + end + + def visit_ChangeColumnDefinition(o) + change_column_sql = "CHANGE #{quote_column_name(o.name)} #{accept(o.column)}" + add_column_position!(change_column_sql, column_options(o.column)) + end + + def column_options(o) + column_options = super + column_options[:charset] = o.charset + column_options + end + + def add_column_options!(sql, options) + if options[:charset] + sql << " CHARACTER SET #{options[:charset]}" + end + if options[:collation] + sql << " COLLATE #{options[:collation]}" + end + super + end + + def add_column_position!(sql, options) + if options[:first] + sql << " FIRST" + elsif options[:after] + sql << " AFTER #{quote_column_name(options[:after])}" + end + sql + end + + def index_in_create(table_name, column_name, options) + index_name, index_type, index_columns, _, _, index_using = @conn.add_index_options(table_name, column_name, options) + "#{index_type} INDEX #{quote_column_name(index_name)} #{index_using} (#{index_columns}) " + end + end + end + end +end diff --git a/activerecord/lib/active_record/connection_adapters/mysql/schema_definitions.rb b/activerecord/lib/active_record/connection_adapters/mysql/schema_definitions.rb new file mode 100644 index 0000000000..ca7dfda80d --- /dev/null +++ b/activerecord/lib/active_record/connection_adapters/mysql/schema_definitions.rb @@ -0,0 +1,69 @@ +module ActiveRecord + module ConnectionAdapters + module MySQL + module ColumnMethods + def primary_key(name, type = :primary_key, **options) + options[:auto_increment] = true if type == :bigint && !options.key?(:default) + super + end + + def blob(*args, **options) + args.each { |name| column(name, :blob, options) } + end + + def json(*args, **options) + args.each { |name| column(name, :json, options) } + end + + def unsigned_integer(*args, **options) + args.each { |name| column(name, :unsigned_integer, options) } + end + + def unsigned_bigint(*args, **options) + args.each { |name| column(name, :unsigned_bigint, options) } + end + + def unsigned_float(*args, **options) + args.each { |name| column(name, :unsigned_float, options) } + end + + def unsigned_decimal(*args, **options) + args.each { |name| column(name, :unsigned_decimal, options) } + end + end + + class ColumnDefinition < ActiveRecord::ConnectionAdapters::ColumnDefinition + attr_accessor :charset, :unsigned + end + + class TableDefinition < ActiveRecord::ConnectionAdapters::TableDefinition + include ColumnMethods + + def new_column_definition(name, type, options) # :nodoc: + column = super + case column.type + when :primary_key + column.type = :integer + column.auto_increment = true + when /\Aunsigned_(?<type>.+)\z/ + column.type = $~[:type].to_sym + column.unsigned = true + end + column.unsigned ||= options[:unsigned] + column.charset = options[:charset] + column + end + + private + + def create_column_definition(name, type) + MySQL::ColumnDefinition.new(name, type) + end + end + + class Table < ActiveRecord::ConnectionAdapters::Table + include ColumnMethods + end + end + end +end diff --git a/activerecord/lib/active_record/connection_adapters/mysql/schema_dumper.rb b/activerecord/lib/active_record/connection_adapters/mysql/schema_dumper.rb new file mode 100644 index 0000000000..9dee3172f4 --- /dev/null +++ b/activerecord/lib/active_record/connection_adapters/mysql/schema_dumper.rb @@ -0,0 +1,59 @@ +module ActiveRecord + module ConnectionAdapters + module MySQL + module ColumnDumper + def column_spec_for_primary_key(column) + spec = {} + if column.bigint? + spec[:id] = ':bigint' + spec[:default] = schema_default(column) || 'nil' unless column.auto_increment? + spec[:unsigned] = 'true' if column.unsigned? + elsif column.auto_increment? + spec[:unsigned] = 'true' if column.unsigned? + return if spec.empty? + else + spec[:id] = column.type.inspect + spec.merge!(prepare_column_options(column).delete_if { |key, _| [:name, :type, :null].include?(key) }) + end + spec + end + + def prepare_column_options(column) + spec = super + spec[:unsigned] = 'true' if column.unsigned? + spec + end + + def migration_keys + super + [:unsigned] + end + + private + + def schema_type(column) + if column.sql_type == 'tinyblob' + 'blob' + else + super + end + end + + def schema_limit(column) + super unless column.type == :boolean + end + + def schema_precision(column) + super unless /time/ === column.sql_type && column.precision == 0 + end + + def schema_collation(column) + if column.collation && table_name = column.instance_variable_get(:@table_name) + @table_collation_cache ||= {} + @table_collation_cache[table_name] ||= select_one("SHOW TABLE STATUS LIKE '#{table_name}'")["Collation"] + column.collation.inspect if column.collation != @table_collation_cache[table_name] + end + end + end + end + end +end diff --git a/activerecord/lib/active_record/connection_adapters/mysql2_adapter.rb b/activerecord/lib/active_record/connection_adapters/mysql2_adapter.rb index 4461722bb4..773ecbe126 100644 --- a/activerecord/lib/active_record/connection_adapters/mysql2_adapter.rb +++ b/activerecord/lib/active_record/connection_adapters/mysql2_adapter.rb @@ -20,7 +20,7 @@ module ActiveRecord ConnectionAdapters::Mysql2Adapter.new(client, logger, options, config) rescue Mysql2::Error => error if error.message.include?("Unknown database") - raise ActiveRecord::NoDatabaseError.new(error.message, error) + raise ActiveRecord::NoDatabaseError else raise end @@ -126,7 +126,9 @@ module ActiveRecord # Returns an array of arrays containing the field values. # Order is the same as that returned by +columns+. def select_rows(sql, name = nil, binds = []) - execute(sql, name).to_a + result = execute(sql, name) + @connection.next_result while @connection.more_results? + result.to_a end # Executes the SQL statement in the context of this connection. @@ -140,8 +142,9 @@ module ActiveRecord super end - def exec_query(sql, name = 'SQL', binds = []) + def exec_query(sql, name = 'SQL', binds = [], prepare: false) result = execute(sql, name) + @connection.next_result while @connection.more_results? ActiveRecord::Result.new(result.fields, result.to_a) end diff --git a/activerecord/lib/active_record/connection_adapters/mysql_adapter.rb b/activerecord/lib/active_record/connection_adapters/mysql_adapter.rb index b3894481cc..89d18ee14e 100644 --- a/activerecord/lib/active_record/connection_adapters/mysql_adapter.rb +++ b/activerecord/lib/active_record/connection_adapters/mysql_adapter.rb @@ -38,7 +38,7 @@ module ActiveRecord ConnectionAdapters::MysqlAdapter.new(mysql, logger, options, config) rescue Mysql::Error => error if error.message.include?("Unknown database") - raise ActiveRecord::NoDatabaseError.new(error.message, error) + raise ActiveRecord::NoDatabaseError else raise end @@ -235,11 +235,11 @@ module ActiveRecord @client_encoding = ENCODINGS[result.rows.last.last] end - def exec_query(sql, name = 'SQL', binds = []) + def exec_query(sql, name = 'SQL', binds = [], prepare: false) if without_prepared_statement?(binds) result_set, affected_rows = exec_without_stmt(sql, name) else - result_set, affected_rows = exec_stmt(sql, name, binds) + result_set, affected_rows = exec_stmt(sql, name, binds, cache_stmt: prepare) end yield affected_rows if block_given? @@ -378,12 +378,12 @@ module ActiveRecord private - def exec_stmt(sql, name, binds) + def exec_stmt(sql, name, binds, cache_stmt: false) cache = {} type_casted_binds = binds.map { |attr| type_cast(attr.value_for_database) } log(sql, name, binds) do - if binds.empty? + if !cache_stmt stmt = @connection.prepare(sql) else cache = @statements[sql] ||= { @@ -399,7 +399,7 @@ module ActiveRecord # place when an error occurs. To support older MySQL versions, we # need to close the statement and delete the statement from the # cache. - if binds.empty? + if !cache_stmt stmt.close else @statements.delete sql @@ -417,7 +417,7 @@ module ActiveRecord affected_rows = stmt.affected_rows stmt.free_result - stmt.close if binds.empty? + stmt.close if !cache_stmt [result_set, affected_rows] 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 index 11d3f5301a..0e0c0e993a 100644 --- a/activerecord/lib/active_record/connection_adapters/postgresql/database_statements.rb +++ b/activerecord/lib/active_record/connection_adapters/postgresql/database_statements.rb @@ -8,7 +8,7 @@ module ActiveRecord end class ExplainPrettyPrinter # :nodoc: - # Pretty prints the result of a EXPLAIN in a way that resembles the output of the + # Pretty prints the result of an EXPLAIN in a way that resembles the output of the # PostgreSQL shell: # # QUERY PLAN @@ -156,8 +156,8 @@ module ActiveRecord end end - def exec_query(sql, name = 'SQL', binds = []) - execute_and_clear(sql, name, binds) do |result| + def exec_query(sql, name = 'SQL', binds = [], prepare: false) + execute_and_clear(sql, name, binds, prepare: prepare) do |result| types = {} fields = result.fields fields.each_with_index do |fname, i| diff --git a/activerecord/lib/active_record/connection_adapters/postgresql/schema_dumper.rb b/activerecord/lib/active_record/connection_adapters/postgresql/schema_dumper.rb new file mode 100644 index 0000000000..a4f0742516 --- /dev/null +++ b/activerecord/lib/active_record/connection_adapters/postgresql/schema_dumper.rb @@ -0,0 +1,54 @@ +module ActiveRecord + module ConnectionAdapters + module PostgreSQL + module ColumnDumper + def column_spec_for_primary_key(column) + spec = {} + if column.serial? + return unless column.bigint? + spec[:id] = ':bigserial' + elsif column.type == :uuid + spec[:id] = ':uuid' + spec[:default] = column.default_function.inspect + else + spec[:id] = column.type.inspect + spec.merge!(prepare_column_options(column).delete_if { |key, _| [:name, :type, :null].include?(key) }) + end + spec + end + + # Adds +:array+ option to the default set + def prepare_column_options(column) + spec = super + spec[:array] = 'true' if column.array? + spec + end + + # Adds +:array+ as a valid migration key + def migration_keys + super + [:array] + end + + private + + def schema_type(column) + return super unless column.serial? + + if column.bigint? + 'bigserial' + else + 'serial' + end + end + + def schema_default(column) + if column.default_function + column.default_function.inspect unless column.serial? + else + super + end + 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 25dfda9ef8..ed6ab8235f 100644 --- a/activerecord/lib/active_record/connection_adapters/postgresql_adapter.rb +++ b/activerecord/lib/active_record/connection_adapters/postgresql_adapter.rb @@ -9,6 +9,7 @@ require "active_record/connection_adapters/postgresql/oid" require "active_record/connection_adapters/postgresql/quoting" require "active_record/connection_adapters/postgresql/referential_integrity" require "active_record/connection_adapters/postgresql/schema_definitions" +require "active_record/connection_adapters/postgresql/schema_dumper" require "active_record/connection_adapters/postgresql/schema_statements" require "active_record/connection_adapters/postgresql/type_metadata" require "active_record/connection_adapters/postgresql/utils" @@ -18,12 +19,6 @@ require 'ipaddr' module ActiveRecord module ConnectionHandling # :nodoc: - VALID_CONN_PARAMS = [:host, :hostaddr, :port, :dbname, :user, :password, :connect_timeout, - :client_encoding, :options, :application_name, :fallback_application_name, - :keepalives, :keepalives_idle, :keepalives_interval, :keepalives_count, - :tty, :sslmode, :requiressl, :sslcompression, :sslcert, :sslkey, - :sslrootcert, :sslcrl, :requirepeer, :krbsrvname, :gsslib, :service] - # Establishes a connection to the database that's used by all Active Record objects def postgresql_connection(config) conn_params = config.symbolize_keys @@ -35,7 +30,8 @@ module ActiveRecord conn_params[:dbname] = conn_params.delete(:database) if conn_params[:database] # Forward only valid config params to PGconn.connect. - conn_params.keep_if { |k, _| VALID_CONN_PARAMS.include?(k) } + valid_conn_param_keys = PGconn.conndefaults_hash.keys + [:requiressl] + conn_params.slice!(*valid_conn_param_keys) # The postgres drivers don't allow the creation of an unconnected PGconn object, # so just pass a nil connection object for the time being. @@ -117,61 +113,14 @@ module ActiveRecord include PostgreSQL::ReferentialIntegrity include PostgreSQL::SchemaStatements include PostgreSQL::DatabaseStatements + include PostgreSQL::ColumnDumper include Savepoints def schema_creation # :nodoc: PostgreSQL::SchemaCreation.new self end - def column_spec_for_primary_key(column) - spec = {} - if column.serial? - return unless column.bigint? - spec[:id] = ':bigserial' - elsif column.type == :uuid - spec[:id] = ':uuid' - spec[:default] = column.default_function.inspect - else - spec[:id] = column.type.inspect - spec.merge!(prepare_column_options(column).delete_if { |key, _| [:name, :type, :null].include?(key) }) - end - spec - end - - # Adds +:array+ option to the default set provided by the - # AbstractAdapter - def prepare_column_options(column) # :nodoc: - spec = super - spec[:array] = 'true' if column.array? - spec - end - - # Adds +:array+ as a valid migration key - def migration_keys - super + [:array] - end - - def schema_type(column) - return super unless column.serial? - - if column.bigint? - 'bigserial' - else - 'serial' - end - end - private :schema_type - - def schema_default(column) - if column.default_function - column.default_function.inspect unless column.serial? - else - super - end - end - private :schema_default - - # Returns +true+, since this connection adapter supports prepared statement + # Returns true, since this connection adapter supports prepared statement # caching. def supports_statement_cache? true @@ -244,6 +193,7 @@ module ActiveRecord @visitor = Arel::Visitors::PostgreSQL.new self if self.class.type_cast_config_to_boolean(config.fetch(:prepared_statements) { true }) @prepared_statements = true + @visitor.extend(DetermineIfPreparableVisitor) else @prepared_statements = false end @@ -326,18 +276,18 @@ module ActiveRecord true end - # Enable standard-conforming strings if available. def set_standard_conforming_strings - old, self.client_min_messages = client_min_messages, 'panic' - execute('SET standard_conforming_strings = on', 'SCHEMA') rescue nil - ensure - self.client_min_messages = old + execute('SET standard_conforming_strings = on', 'SCHEMA') end def supports_ddl_transactions? true end + def supports_advisory_locks? + true + end + def supports_explain? true end @@ -356,6 +306,20 @@ module ActiveRecord postgresql_version >= 90300 end + def get_advisory_lock(key) # :nodoc: + unless key.is_a?(Integer) && key.bit_length <= 63 + raise(ArgumentError, "Postgres requires advisory lock keys to be a signed 64 bit integer") + end + select_value("SELECT pg_try_advisory_lock(#{key});") + end + + def release_advisory_lock(key) # :nodoc: + unless key.is_a?(Integer) && key.bit_length <= 63 + raise(ArgumentError, "Postgres requires advisory lock keys to be a signed 64 bit integer") + end + select_value("SELECT pg_advisory_unlock(#{key})") + end + def enable_extension(name) exec_query("CREATE EXTENSION IF NOT EXISTS \"#{name}\"").tap { reload_type_map @@ -438,9 +402,9 @@ module ActiveRecord case exception.result.try(:error_field, PGresult::PG_DIAG_SQLSTATE) when UNIQUE_VIOLATION - RecordNotUnique.new(message, exception) + RecordNotUnique.new(message) when FOREIGN_KEY_VIOLATION - InvalidForeignKey.new(message, exception) + InvalidForeignKey.new(message) else super end @@ -599,16 +563,22 @@ module ActiveRecord FEATURE_NOT_SUPPORTED = "0A000" #:nodoc: - def execute_and_clear(sql, name, binds) - result = without_prepared_statement?(binds) ? exec_no_cache(sql, name, binds) : - exec_cache(sql, name, binds) + def execute_and_clear(sql, name, binds, prepare: false) + if without_prepared_statement?(binds) + result = exec_no_cache(sql, name, []) + elsif !prepare + result = exec_no_cache(sql, name, binds) + else + result = exec_cache(sql, name, binds) + end ret = yield result result.clear ret end def exec_no_cache(sql, name, binds) - log(sql, name, binds) { @connection.async_exec(sql, []) } + type_casted_binds = binds.map { |attr| type_cast(attr.value_for_database) } + log(sql, name, binds) { @connection.async_exec(sql, type_casted_binds) } end def exec_cache(sql, name, binds) @@ -619,7 +589,7 @@ module ActiveRecord @connection.exec_prepared(stmt_key, type_casted_binds) end rescue ActiveRecord::StatementInvalid => e - pgerror = e.original_exception + pgerror = e.cause # Get the PG code for the failure. Annoyingly, the code for # prepared statements whose return value may have changed is @@ -675,7 +645,7 @@ module ActiveRecord configure_connection rescue ::PG::Error => error if error.message.include?("does not exist") - raise ActiveRecord::NoDatabaseError.new(error.message, error) + raise ActiveRecord::NoDatabaseError else raise end @@ -690,7 +660,7 @@ module ActiveRecord 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. + # Use standard-conforming strings so we don't have to do the E'...' dance. set_standard_conforming_strings # If using Active Record's time zone support configure the connection to return diff --git a/activerecord/lib/active_record/connection_adapters/sqlite3_adapter.rb b/activerecord/lib/active_record/connection_adapters/sqlite3_adapter.rb index f6a6e3eb4d..32fe275bfb 100644 --- a/activerecord/lib/active_record/connection_adapters/sqlite3_adapter.rb +++ b/activerecord/lib/active_record/connection_adapters/sqlite3_adapter.rb @@ -33,7 +33,7 @@ module ActiveRecord ConnectionAdapters::SQLite3Adapter.new(db, logger, nil, config) rescue Errno::ENOENT => error if error.message.include?("No such file or directory") - raise ActiveRecord::NoDatabaseError.new(error.message, error) + raise ActiveRecord::NoDatabaseError else raise end @@ -89,6 +89,7 @@ module ActiveRecord if self.class.type_cast_config_to_boolean(config.fetch(:prepared_statements) { true }) @prepared_statements = true + @visitor.extend(DetermineIfPreparableVisitor) else @prepared_statements = false end @@ -218,7 +219,7 @@ module ActiveRecord end class ExplainPrettyPrinter - # Pretty prints the result of a EXPLAIN QUERY PLAN in a way that resembles + # Pretty prints the result of an 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) @@ -231,15 +232,18 @@ module ActiveRecord end end - def exec_query(sql, name = nil, binds = []) + def exec_query(sql, name = nil, binds = [], prepare: false) type_casted_binds = binds.map { |attr| type_cast(attr.value_for_database) } log(sql, name, binds) do # Don't cache statements if they are not prepared - if without_prepared_statement?(binds) + unless prepare stmt = @connection.prepare(sql) begin cols = stmt.columns + unless without_prepared_statement?(binds) + stmt.bind_params(type_casted_binds) + end records = stmt.to_a ensure stmt.close @@ -252,7 +256,7 @@ module ActiveRecord stmt = cache[:stmt] cols = cache[:cols] ||= stmt.columns stmt.reset! - stmt.bind_params type_casted_binds + stmt.bind_params(type_casted_binds) end ActiveRecord::Result.new(cols, stmt.to_a) @@ -307,22 +311,18 @@ module ActiveRecord # SCHEMA STATEMENTS ======================================== - def tables(name = nil, table_name = nil) #:nodoc: - sql = <<-SQL - SELECT name - FROM sqlite_master - WHERE (type = 'table' OR type = 'view') 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 + def tables(name = nil) # :nodoc: + select_values("SELECT name FROM sqlite_master WHERE type IN ('table','view') AND name <> 'sqlite_sequence'", 'SCHEMA') end alias data_sources tables def table_exists?(table_name) - table_name && tables(nil, table_name).any? + return false unless table_name.present? + + sql = "SELECT name FROM sqlite_master WHERE type IN ('table','view') AND name <> 'sqlite_sequence'" + sql << " AND name = #{quote(table_name)}" + + select_values(sql, 'SCHEMA').any? end alias data_source_exists? table_exists? @@ -559,7 +559,7 @@ module ActiveRecord # Older versions of SQLite return: # column *column_name* is not unique when /column(s)? .* (is|are) not unique/, /UNIQUE constraint failed: .*/ - RecordNotUnique.new(message, exception) + RecordNotUnique.new(message) else super end diff --git a/activerecord/lib/active_record/connection_handling.rb b/activerecord/lib/active_record/connection_handling.rb index d6b661ff76..aedef54928 100644 --- a/activerecord/lib/active_record/connection_handling.rb +++ b/activerecord/lib/active_record/connection_handling.rb @@ -35,14 +35,14 @@ module ActiveRecord # "postgres://myuser:mypass@localhost/somedatabase" # ) # - # In case <tt>ActiveRecord::Base.configurations</tt> is set (Rails - # automatically loads the contents of config/database.yml into it), + # In case {ActiveRecord::Base.configurations}[rdoc-ref:Core.configurations] + # is set (Rails automatically loads the contents of config/database.yml into it), # a symbol can also be given as argument, representing a key in the # configuration hash: # # ActiveRecord::Base.establish_connection(:production) # - # The exceptions AdapterNotSpecified, AdapterNotFound and ArgumentError + # The exceptions AdapterNotSpecified, AdapterNotFound and +ArgumentError+ # may be returned on an error. def establish_connection(spec = nil) spec ||= DEFAULT_ENV.call.to_sym diff --git a/activerecord/lib/active_record/core.rb b/activerecord/lib/active_record/core.rb index 894d18b79e..1250f8a3c3 100644 --- a/activerecord/lib/active_record/core.rb +++ b/activerecord/lib/active_record/core.rb @@ -177,7 +177,7 @@ module ActiveRecord hash = args.first return super if hash.values.any? { |v| - v.nil? || Array === v || Hash === v + v.nil? || Array === v || Hash === v || Relation === v } # We can't cache Post.find_by(author: david) ...yet @@ -193,8 +193,8 @@ module ActiveRecord } begin statement.execute(hash.values, self, connection).first - rescue TypeError => e - raise ActiveRecord::StatementInvalid.new(e.message, e) + rescue TypeError + raise ActiveRecord::StatementInvalid rescue RangeError nil end @@ -310,7 +310,7 @@ module ActiveRecord # Initialize an empty model object from +coder+. +coder+ should be # the result of previously encoding an Active Record model, using - # `encode_with` + # #encode_with. # # class Post < ActiveRecord::Base # end @@ -379,7 +379,7 @@ module ActiveRecord # Populate +coder+ with attributes about this record that should be # serialized. The structure of +coder+ defined in this method is - # guaranteed to match the structure of +coder+ passed to the +init_with+ + # guaranteed to match the structure of +coder+ passed to the #init_with # method. # # Example: @@ -477,7 +477,7 @@ module ActiveRecord "#<#{self.class} #{inspection}>" end - # Takes a PP and prettily prints this record to it, allowing you to get a nice result from `pp record` + # Takes a PP and prettily prints this record to it, allowing you to get a nice result from <tt>pp record</tt> # when pp is required. def pretty_print(pp) return super if custom_inspect_method_defined? diff --git a/activerecord/lib/active_record/counter_cache.rb b/activerecord/lib/active_record/counter_cache.rb index 82596b63df..9e7d391c70 100644 --- a/activerecord/lib/active_record/counter_cache.rb +++ b/activerecord/lib/active_record/counter_cache.rb @@ -45,14 +45,14 @@ module ActiveRecord end # A generic "counter updater" implementation, intended primarily to be - # used by increment_counter and decrement_counter, but which may also + # 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. + # * +id+ - The id of the object you wish to update a counter on or an array of ids. # * +counters+ - A Hash containing the names of the fields # to update as keys and the amount to update the field by as values. # @@ -86,14 +86,14 @@ module ActiveRecord # Increment a numeric field by one, via a direct SQL update. # # This method is used primarily for maintaining counter_cache columns that are - # used to store aggregate values. For example, a DiscussionBoard may cache + # used to store aggregate values. For example, a +DiscussionBoard+ may cache # posts_count and comments_count to avoid running an SQL query to calculate the # number of posts and comments there are, each time it is displayed. # # ==== Parameters # # * +counter_name+ - The name of the field that should be incremented. - # * +id+ - The id of the object that should be incremented or an Array of ids. + # * +id+ - The id of the object that should be incremented or an array of ids. # # ==== Examples # @@ -105,13 +105,13 @@ module ActiveRecord # Decrement a numeric field by one, via a direct SQL update. # - # This works the same as increment_counter but reduces the column value by + # 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 or an Array of ids. + # * +id+ - The id of the object that should be decremented or an array of ids. # # ==== Examples # diff --git a/activerecord/lib/active_record/enum.rb b/activerecord/lib/active_record/enum.rb index 10b5fcab24..8fba6fcc35 100644 --- a/activerecord/lib/active_record/enum.rb +++ b/activerecord/lib/active_record/enum.rb @@ -46,13 +46,13 @@ module ActiveRecord # Good practice is to let the first declared status be the default. # # Finally, it's also possible to explicitly map the relation between attribute and - # database integer with a +Hash+: + # database integer with a hash: # # class Conversation < ActiveRecord::Base # enum status: { active: 0, archived: 1 } # end # - # Note that when an +Array+ is used, the implicit mapping from the values to database + # Note that when an array is used, the implicit mapping from the values to database # integers is derived from the order the values appear in the array. In the example, # <tt>:active</tt> is mapped to +0+ as it's the first element, and <tt>:archived</tt> # is mapped to +1+. In general, the +i+-th element is mapped to <tt>i-1</tt> in the @@ -60,7 +60,7 @@ module ActiveRecord # # Therefore, once a value is added to the enum array, its position in the array must # be maintained, and new values should only be added to the end of the array. To - # remove unused values, the explicit +Hash+ syntax should be used. + # remove unused values, the explicit hash syntax should be used. # # In rare circumstances you might need to access the mapping directly. # The mappings are exposed through a class method with the pluralized attribute diff --git a/activerecord/lib/active_record/errors.rb b/activerecord/lib/active_record/errors.rb index 6721fe144f..1cd2c2ef8c 100644 --- a/activerecord/lib/active_record/errors.rb +++ b/activerecord/lib/active_record/errors.rb @@ -7,8 +7,10 @@ module ActiveRecord end # Raised when the single-table inheritance mechanism fails to locate the subclass - # (for example due to improper usage of column that +inheritance_column+ points to). - class SubclassNotFound < ActiveRecordError #:nodoc: + # (for example due to improper usage of column that + # {ActiveRecord::Base.inheritance_column}[rdoc-ref:ModelSchema::ClassMethods#inheritance_column] + # points to). + class SubclassNotFound < ActiveRecordError end # Raised when an object assigned to an association has an incorrect type. @@ -40,12 +42,13 @@ module ActiveRecord class AdapterNotFound < ActiveRecordError end - # Raised when connection to the database could not been established (for - # example when +connection=+ is given a nil object). + # Raised when connection to the database could not been established (for example when + # {ActiveRecord::Base.connection=}[rdoc-ref:ConnectionHandling#connection] + # is given a nil object). class ConnectionNotEstablished < ActiveRecordError end - # Raised when Active Record cannot find record by given id or set of ids. + # Raised when Active Record cannot find a record by given id or set of ids. class RecordNotFound < ActiveRecordError attr_reader :model, :primary_key, :id @@ -58,8 +61,9 @@ module ActiveRecord end end - # Raised by ActiveRecord::Base.save! and ActiveRecord::Base.create! methods when record cannot be - # saved because record is invalid. + # Raised by {ActiveRecord::Base#save!}[rdoc-ref:Persistence#save!] and + # {ActiveRecord::Base.create!}[rdoc-ref:Persistence::ClassMethods#create!] + # methods when a record is invalid and can not be saved. class RecordNotSaved < ActiveRecordError attr_reader :record @@ -69,7 +73,9 @@ module ActiveRecord end end - # Raised by ActiveRecord::Base.destroy! when a call to destroy would return false. + # Raised by {ActiveRecord::Base#destroy!}[rdoc-ref:Persistence#destroy!] + # when a call to {#destroy}[rdoc-ref:Persistence#destroy!] + # would return false. # # begin # complex_operation_that_internally_calls_destroy! @@ -88,18 +94,26 @@ module ActiveRecord # Superclass for all database execution errors. # - # Wraps the underlying database error as +original_exception+. + # Wraps the underlying database error as +cause+. class StatementInvalid < ActiveRecordError - attr_reader :original_exception def initialize(message = nil, original_exception = nil) - @original_exception = original_exception - super(message) + if original_exception + ActiveSupport::Deprecation.warn("Passing #original_exception is deprecated and has no effect. " \ + "Exceptions will automatically capture the original exception.", caller) + end + + super(message || $!.try(:message)) + end + + def original_exception + ActiveSupport::Deprecation.warn("#original_exception is deprecated. Use #cause instead.", caller) + cause end end # Defunct wrapper class kept for compatibility. - # +StatementInvalid+ wraps the original exception now. + # StatementInvalid wraps the original exception now. class WrappedDatabaseException < StatementInvalid end @@ -112,8 +126,8 @@ module ActiveRecord end # Raised when number of bind variables in statement given to +:condition+ key - # (for example, when using +find+ method) does not match number of expected - # values supplied. + # (for example, when using {ActiveRecord::Base.find}[rdoc-ref:FinderMethods#find] method) + # does not match number of expected values supplied. # # For example, when there are two placeholders with only one value supplied: # @@ -147,7 +161,9 @@ module ActiveRecord end # Raised when association is being configured improperly or user tries to use - # offset and limit together with +has_many+ or +has_and_belongs_to_many+ + # offset and limit together with + # {ActiveRecord::Base.has_many}[rdoc-ref:Associations::ClassMethods#has_many] or + # {ActiveRecord::Base.has_and_belongs_to_many}[rdoc-ref:Associations::ClassMethods#has_and_belongs_to_many] # associations. class ConfigurationError < ActiveRecordError end @@ -156,9 +172,10 @@ module ActiveRecord class ReadOnlyRecord < ActiveRecordError end - # ActiveRecord::Transactions::ClassMethods.transaction uses this exception - # to distinguish a deliberate rollback from other exceptional situations. - # Normally, raising an exception will cause the +transaction+ method to rollback + # {ActiveRecord::Base.transaction}[rdoc-ref:Transactions::ClassMethods#transaction] + # uses this exception to distinguish a deliberate rollback from other exceptional situations. + # Normally, raising an exception will cause the + # {.transaction}[rdoc-ref:Transactions::ClassMethods#transaction] method to rollback # the database transaction *and* pass on the exception. But if you raise an # ActiveRecord::Rollback exception, then the database transaction will be rolled back, # without passing on the exception. @@ -195,8 +212,8 @@ module ActiveRecord UnknownAttributeError = ActiveModel::UnknownAttributeError # Raised when an error occurred while doing a mass assignment to an attribute through the - # +attributes=+ method. The exception has an +attribute+ property that is the name of the - # offending attribute. + # {ActiveRecord::Base#attributes=}[rdoc-ref:AttributeAssignment#attributes=] method. + # The exception has an +attribute+ property that is the name of the offending attribute. class AttributeAssignmentError < ActiveRecordError attr_reader :exception, :attribute @@ -207,7 +224,8 @@ module ActiveRecord end end - # Raised when there are multiple errors while doing a mass assignment through the +attributes+ + # Raised when there are multiple errors while doing a mass assignment through the + # {ActiveRecord::Base#attributes=}[rdoc-ref:AttributeAssignment#attributes=] # method. The exception has an +errors+ property that contains an array of AttributeAssignmentError # objects, each corresponding to the error while assigning to an attribute. class MultiparameterAssignmentErrors < ActiveRecordError diff --git a/activerecord/lib/active_record/explain_registry.rb b/activerecord/lib/active_record/explain_registry.rb index f5cd57e075..b652932f9c 100644 --- a/activerecord/lib/active_record/explain_registry.rb +++ b/activerecord/lib/active_record/explain_registry.rb @@ -7,7 +7,7 @@ module ActiveRecord # # returns the collected queries local to the current thread. # - # See the documentation of <tt>ActiveSupport::PerThreadRegistry</tt> + # See the documentation of ActiveSupport::PerThreadRegistry # for further details. class ExplainRegistry # :nodoc: extend ActiveSupport::PerThreadRegistry diff --git a/activerecord/lib/active_record/explain_subscriber.rb b/activerecord/lib/active_record/explain_subscriber.rb index 9adabd7819..90bcf5a205 100644 --- a/activerecord/lib/active_record/explain_subscriber.rb +++ b/activerecord/lib/active_record/explain_subscriber.rb @@ -14,7 +14,7 @@ module ActiveRecord end # SCHEMA queries cannot be EXPLAINed, also we do not want to run EXPLAIN on - # our own EXPLAINs now matter how loopingly beautiful that would be. + # our own EXPLAINs no matter how loopingly beautiful that would be. # # 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. diff --git a/activerecord/lib/active_record/fixture_set/file.rb b/activerecord/lib/active_record/fixture_set/file.rb index 8132310c91..f969556c50 100644 --- a/activerecord/lib/active_record/fixture_set/file.rb +++ b/activerecord/lib/active_record/fixture_set/file.rb @@ -17,24 +17,39 @@ module ActiveRecord def initialize(file) @file = file - @rows = nil end def each(&block) rows.each(&block) end + def model_class + config_row['model_class'] + end private def rows - return @rows if @rows + @rows ||= raw_rows.reject { |fixture_name, _| fixture_name == '_fixture' } + end + + def config_row + @config_row ||= begin + row = raw_rows.find { |fixture_name, _| fixture_name == '_fixture' } + if row + row.last + else + {'model_class': nil} + end + end + end - begin + def raw_rows + @raw_rows ||= begin data = YAML.load(render(IO.read(@file))) + data ? validate(data).to_a : [] rescue ArgumentError, Psych::SyntaxError => 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 def render(content) diff --git a/activerecord/lib/active_record/fixtures.rb b/activerecord/lib/active_record/fixtures.rb index f1dc56df63..deb7d7d06d 100644 --- a/activerecord/lib/active_record/fixtures.rb +++ b/activerecord/lib/active_record/fixtures.rb @@ -89,7 +89,7 @@ module ActiveRecord # end # # In order to use these methods to access fixtured data within your testcases, you must specify one of the - # following in your <tt>ActiveSupport::TestCase</tt>-derived class: + # following in your ActiveSupport::TestCase-derived class: # # - to fully enable instantiated fixtures (enable alternate methods #1 and #2 above) # self.use_instantiated_fixtures = true @@ -124,7 +124,7 @@ module ActiveRecord # # Helper methods defined in a fixture will not be available in other fixtures, to prevent against # unwanted inter-test dependencies. Methods used by multiple fixtures should be defined in a module - # that is included in <tt>ActiveRecord::FixtureSet.context_class</tt>. + # that is included in ActiveRecord::FixtureSet.context_class. # # - define a helper method in `test_helper.rb` # module FixtureFileHelpers @@ -395,6 +395,20 @@ module ActiveRecord # <<: *DEFAULTS # # Any fixture labeled "DEFAULTS" is safely ignored. + # + # == Configure the fixture model class + # + # It's possible to set the fixture's model class directly in the YAML file. + # This is helpful when fixtures are loaded outside tests and + # +set_fixture_class+ is not available (e.g. + # when running <tt>rake db:fixtures:load</tt>). + # + # _fixture: + # model_class: User + # david: + # name: David + # + # Any fixtures labeled "_fixture" are safely ignored. class FixtureSet #-- # An instance of FixtureSet is normally stored in a single YAML file and @@ -578,21 +592,16 @@ module ActiveRecord @name = name @path = path @config = config - @model_class = nil - 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.safe_constantize if class_name - end + self.model_class = class_name + + @fixtures = read_fixture_files(path) @connection = connection @table_name = ( model_class.respond_to?(:table_name) ? model_class.table_name : self.class.default_fixture_table_name(name, config) ) - - @fixtures = read_fixture_files path, @model_class end def [](x) @@ -761,13 +770,25 @@ module ActiveRecord @column_names ||= @connection.columns(@table_name).collect(&:name) end - def read_fixture_files(path, model_class) + def model_class=(class_name) + 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.safe_constantize if class_name + end + end + + # Loads the fixtures from the YAML file at +path+. + # If the file sets the +model_class+ and current instance value is not set, + # it uses the file value. + def read_fixture_files(path) yaml_files = Dir["#{path}/{**,*}/*.yml"].select { |f| ::File.file?(f) } + [yaml_file_path(path)] yaml_files.each_with_object({}) do |file, fixtures| FixtureSet::File.open(file) do |fh| + self.model_class ||= fh.model_class if fh.model_class fh.each do |fixture_name, row| fixtures[fixture_name] = ActiveRecord::Fixture.new(row, model_class) end @@ -854,9 +875,7 @@ module ActiveRecord self.pre_loaded_fixtures = false self.config = ActiveRecord::Base - self.fixture_class_names = Hash.new do |h, fixture_set_name| - h[fixture_set_name] = ActiveRecord::FixtureSet.default_fixture_model_name(fixture_set_name, self.config) - end + self.fixture_class_names = {} silence_warnings do define_singleton_method :use_transactional_tests do diff --git a/activerecord/lib/active_record/inheritance.rb b/activerecord/lib/active_record/inheritance.rb index c26842014d..589c70db0d 100644 --- a/activerecord/lib/active_record/inheritance.rb +++ b/activerecord/lib/active_record/inheritance.rb @@ -55,7 +55,7 @@ module ActiveRecord subclass = subclass_from_attributes(attrs) end - if subclass + if subclass && subclass != self subclass.new(*args, &block) else super @@ -167,17 +167,23 @@ module ActiveRecord end def find_sti_class(type_name) - if store_full_sti_class - ActiveSupport::Dependencies.constantize(type_name) - else - compute_type(type_name) + subclass = begin + if store_full_sti_class + ActiveSupport::Dependencies.constantize(type_name) + else + compute_type(type_name) + end + rescue NameError + raise SubclassNotFound, + "The single-table inheritance mechanism failed to locate the subclass: '#{type_name}'. " \ + "This error is raised because the column '#{inheritance_column}' is reserved for storing the class in case of inheritance. " \ + "Please rename this column if you didn't intend it to be used for storing the inheritance class " \ + "or overwrite #{name}.inheritance_column to use another column for that information." + end + unless subclass == self || descendants.include?(subclass) + raise SubclassNotFound, "Invalid single-table inheritance type: #{subclass.name} is not a subclass of #{name}" end - rescue NameError - raise SubclassNotFound, - "The single-table inheritance mechanism failed to locate the subclass: '#{type_name}'. " + - "This error is raised because the column '#{inheritance_column}' is reserved for storing the class in case of inheritance. " + - "Please rename this column if you didn't intend it to be used for storing the inheritance class " + - "or overwrite #{name}.inheritance_column to use another column for that information." + subclass end def type_condition(table = arel_table) @@ -199,15 +205,7 @@ module ActiveRecord subclass_name = attrs.with_indifferent_access[inheritance_column] if subclass_name.present? - subclass = find_sti_class(subclass_name) - - if subclass.name != self.name - unless descendants.include?(subclass) - raise ActiveRecord::SubclassNotFound.new("Invalid single-table inheritance type: #{subclass.name} is not a subclass of #{name}") - end - - subclass - end + find_sti_class(subclass_name) end end end diff --git a/activerecord/lib/active_record/integration.rb b/activerecord/lib/active_record/integration.rb index 370c1562c9..466c8509a4 100644 --- a/activerecord/lib/active_record/integration.rb +++ b/activerecord/lib/active_record/integration.rb @@ -10,12 +10,12 @@ module ActiveRecord # Indicates the format used to generate the timestamp in the cache key. # Accepts any of the symbols in <tt>Time::DATE_FORMATS</tt>. # - # This is +:nsec+, by default. + # This is +:usec+, by default. class_attribute :cache_timestamp_format, :instance_writer => false - self.cache_timestamp_format = :nsec + self.cache_timestamp_format = :usec end - # Returns a String, which Action Pack uses for constructing an URL to this + # Returns a String, which Action Pack uses for constructing a URL to this # object. The default implementation returns this record's id as a String, # or nil if this record's unsaved. # diff --git a/activerecord/lib/active_record/migration.rb b/activerecord/lib/active_record/migration.rb index 0c50826c63..1e870d4c44 100644 --- a/activerecord/lib/active_record/migration.rb +++ b/activerecord/lib/active_record/migration.rb @@ -135,6 +135,14 @@ module ActiveRecord end end + class ConcurrentMigrationError < MigrationError #:nodoc: + DEFAULT_MESSAGE = "Cannot run migrations because another migration process is currently running.".freeze + + def initialize(message = DEFAULT_MESSAGE) + super + end + end + # = Active Record Migrations # # Migrations can manage the evolution of a schema used by several physical @@ -194,17 +202,18 @@ module ActiveRecord # # == Available transformations # + # === Creation + # + # * <tt>create_join_table(table_1, table_2, options)</tt>: Creates a join + # table having its name as the lexical order of the first two + # arguments. See + # ActiveRecord::ConnectionAdapters::SchemaStatements#create_join_table for + # details. # * <tt>create_table(name, options)</tt>: Creates a table called +name+ and # makes the table object available to a block that can then add columns to it, # following the same format as +add_column+. See example above. The options hash # is for fragments like "DEFAULT CHARSET=UTF-8" that are appended to the create # table definition. - # * <tt>drop_table(name)</tt>: Drops the table called +name+. - # * <tt>change_table(name, options)</tt>: Allows to make column alterations to - # the table called +name+. It makes the table object available to a block that - # can then add/remove columns, indexes or foreign keys to it. - # * <tt>rename_table(old_name, new_name)</tt>: Renames the table called +old_name+ - # to +new_name+. # * <tt>add_column(table_name, column_name, type, options)</tt>: Adds a new column # to the table called +table_name+ # named +column_name+ specified to be one of the following types: @@ -215,24 +224,59 @@ module ActiveRecord # Other options include <tt>:limit</tt> and <tt>:null</tt> (e.g. # <tt>{ limit: 50, null: false }</tt>) -- see # ActiveRecord::ConnectionAdapters::TableDefinition#column for details. - # * <tt>rename_column(table_name, column_name, new_column_name)</tt>: Renames - # a column but keeps the type and content. - # * <tt>change_column(table_name, column_name, type, options)</tt>: Changes - # the column to a different type using the same parameters as add_column. - # * <tt>remove_column(table_name, column_name, type, options)</tt>: Removes the column - # named +column_name+ from the table called +table_name+. + # * <tt>add_foreign_key(from_table, to_table, options)</tt>: Adds a new + # foreign key. +from_table+ is the table with the key column, +to_table+ contains + # the referenced primary key. # * <tt>add_index(table_name, column_names, options)</tt>: Adds a new index # with the name of the column. Other options include # <tt>:name</tt>, <tt>:unique</tt> (e.g. # <tt>{ name: 'users_name_index', unique: true }</tt>) and <tt>:order</tt> # (e.g. <tt>{ order: { name: :desc } }</tt>). + # * <tt>add_reference(:table_name, :reference_name)</tt>: Adds a new column + # +reference_name_id+ by default an integer. See + # ActiveRecord::ConnectionAdapters::SchemaStatements#add_reference for details. + # * <tt>add_timestamps(table_name, options)</tt>: Adds timestamps (+created_at+ + # and +updated_at+) columns to +table_name+. + # + # === Modification + # + # * <tt>change_column(table_name, column_name, type, options)</tt>: Changes + # the column to a different type using the same parameters as add_column. + # * <tt>change_column_default(table_name, column_name, default)</tt>: Sets a + # default value for +column_name+ definded by +default+ on +table_name+. + # * <tt>change_column_null(table_name, column_name, null, default = nil)</tt>: + # Sets or removes a +NOT NULL+ constraint on +column_name+. The +null+ flag + # indicates whether the value can be +NULL+. See + # ActiveRecord::ConnectionAdapters::SchemaStatements#change_column_null for + # details. + # * <tt>change_table(name, options)</tt>: Allows to make column alterations to + # the table called +name+. It makes the table object available to a block that + # can then add/remove columns, indexes or foreign keys to it. + # * <tt>rename_column(table_name, column_name, new_column_name)</tt>: Renames + # a column but keeps the type and content. + # * <tt>rename_index(table_name, old_name, new_name)</tt>: Renames an index. + # * <tt>rename_table(old_name, new_name)</tt>: Renames the table called +old_name+ + # to +new_name+. + # + # === Deletion + # + # * <tt>drop_table(name)</tt>: Drops the table called +name+. + # * <tt>drop_join_table(table_1, table_2, options)</tt>: Drops the join table + # specified by the given arguments. + # * <tt>remove_column(table_name, column_name, type, options)</tt>: Removes the column + # named +column_name+ from the table called +table_name+. + # * <tt>remove_columns(table_name, *column_names)</tt>: Removes the given + # columns from the table definition. + # * <tt>remove_foreign_key(from_table, options_or_to_table)</tt>: Removes the + # given foreign key from the table called +table_name+. # * <tt>remove_index(table_name, column: column_names)</tt>: Removes the index # specified by +column_names+. # * <tt>remove_index(table_name, name: index_name)</tt>: Removes the index # specified by +index_name+. - # * <tt>add_reference(:table_name, :reference_name)</tt>: Adds a new column - # +reference_name_id+ by default an integer. See - # ActiveRecord::ConnectionAdapters::SchemaStatements#add_reference for details. + # * <tt>remove_reference(table_name, ref_name, options)</tt>: Removes the + # reference(s) on +table_name+ specified by +ref_name+. + # * <tt>remove_timestamps(table_name, options)</tt>: Removes the timestamp + # columns (+created_at+ and +updated_at+) from the table definition. # # == Irreversible transformations # @@ -433,6 +477,7 @@ module ActiveRecord class Migration autoload :CommandRecorder, 'active_record/migration/command_recorder' + MigrationFilenameRegexp = /\A([0-9]+)_([_a-z0-9]*)\.?([_a-z0-9]*)?\.rb\z/ # :nodoc: # 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 @@ -947,18 +992,25 @@ module ActiveRecord def migrations_paths @migrations_paths ||= ['db/migrate'] - # just to not break things if someone uses: migration_path = some_string + # just to not break things if someone uses: migrations_path = some_string Array(@migrations_paths) end + def match_to_migration_filename?(filename) # :nodoc: + File.basename(filename) =~ Migration::MigrationFilenameRegexp + end + + def parse_migration_filename(filename) # :nodoc: + File.basename(filename).scan(Migration::MigrationFilenameRegexp).first + end + def migrations(paths) paths = Array(paths) files = Dir[*paths.map { |p| "#{p}/**/[0-9]*_*.rb" }] migrations = files.map do |file| - version, name, scope = file.scan(/([0-9]+)_([_a-z0-9]*)\.?([_a-z0-9]*)?\.rb\z/).first - + version, name, scope = parse_migration_filename(file) raise IllegalMigrationNameError.new(file) unless version version = version.to_i name = name.camelize @@ -1006,32 +1058,18 @@ module ActiveRecord alias :current :current_migration def run - migration = migrations.detect { |m| m.version == @target_version } - raise UnknownMigrationVersionError.new(@target_version) if migration.nil? - unless (up? && migrated.include?(migration.version.to_i)) || (down? && !migrated.include?(migration.version.to_i)) - begin - execute_migration_in_transaction(migration, @direction) - rescue => e - canceled_msg = use_transaction?(migration) ? ", this migration was canceled" : "" - raise StandardError, "An error has occurred#{canceled_msg}:\n\n#{e}", e.backtrace - end + if use_advisory_lock? + with_advisory_lock { run_without_lock } + else + run_without_lock end end def migrate - if !target && @target_version && @target_version > 0 - raise UnknownMigrationVersionError.new(@target_version) - end - - runnable.each do |migration| - Base.logger.info "Migrating to #{migration.name} (#{migration.version})" if Base.logger - - begin - execute_migration_in_transaction(migration, @direction) - rescue => e - canceled_msg = use_transaction?(migration) ? "this and " : "" - raise StandardError, "An error has occurred, #{canceled_msg}all later migrations canceled:\n\n#{e}", e.backtrace - end + if use_advisory_lock? + with_advisory_lock { migrate_without_lock } + else + migrate_without_lock end end @@ -1056,10 +1094,45 @@ module ActiveRecord end def migrated - @migrated_versions ||= Set.new(self.class.get_all_versions) + @migrated_versions || load_migrated + end + + def load_migrated + @migrated_versions = Set.new(self.class.get_all_versions) end private + + def run_without_lock + migration = migrations.detect { |m| m.version == @target_version } + raise UnknownMigrationVersionError.new(@target_version) if migration.nil? + unless (up? && migrated.include?(migration.version.to_i)) || (down? && !migrated.include?(migration.version.to_i)) + begin + execute_migration_in_transaction(migration, @direction) + rescue => e + canceled_msg = use_transaction?(migration) ? ", this migration was canceled" : "" + raise StandardError, "An error has occurred#{canceled_msg}:\n\n#{e}", e.backtrace + end + end + end + + def migrate_without_lock + if !target && @target_version && @target_version > 0 + raise UnknownMigrationVersionError.new(@target_version) + end + + runnable.each do |migration| + Base.logger.info "Migrating to #{migration.name} (#{migration.version})" if Base.logger + + begin + execute_migration_in_transaction(migration, @direction) + rescue => e + canceled_msg = use_transaction?(migration) ? "this and " : "" + raise StandardError, "An error has occurred, #{canceled_msg}all later migrations canceled:\n\n#{e}", e.backtrace + end + end + end + def ran?(migration) migrated.include?(migration.version.to_i) end @@ -1121,5 +1194,25 @@ module ActiveRecord def use_transaction?(migration) !migration.disable_ddl_transaction && Base.connection.supports_ddl_transactions? end + + def use_advisory_lock? + Base.connection.supports_advisory_locks? + end + + def with_advisory_lock + key = generate_migrator_advisory_lock_key + got_lock = Base.connection.get_advisory_lock(key) + raise ConcurrentMigrationError unless got_lock + load_migrated # reload schema_migrations to be sure it wasn't changed by another process before we got the lock + yield + ensure + Base.connection.release_advisory_lock(key) if got_lock + end + + MIGRATOR_SALT = 2053462845 + def generate_migrator_advisory_lock_key + db_name_hash = Zlib.crc32(Base.connection.current_database) + MIGRATOR_SALT * db_name_hash + end end end diff --git a/activerecord/lib/active_record/migration/command_recorder.rb b/activerecord/lib/active_record/migration/command_recorder.rb index 4c4afb4dbd..0fa665c7e0 100644 --- a/activerecord/lib/active_record/migration/command_recorder.rb +++ b/activerecord/lib/active_record/migration/command_recorder.rb @@ -9,6 +9,7 @@ module ActiveRecord # * add_index # * add_reference # * add_timestamps + # * change_column # * change_column_default (must supply a :from and :to option) # * change_column_null # * create_join_table @@ -18,6 +19,7 @@ module ActiveRecord # * drop_table (must supply a block) # * enable_extension # * remove_column (must supply a type) + # * remove_columns (must specify at least one column name or more) # * remove_foreign_key (must supply a second table) # * remove_index # * remove_reference diff --git a/activerecord/lib/active_record/model_schema.rb b/activerecord/lib/active_record/model_schema.rb index a9bd094a66..e3f304b0af 100644 --- a/activerecord/lib/active_record/model_schema.rb +++ b/activerecord/lib/active_record/model_schema.rb @@ -339,6 +339,9 @@ module ActiveRecord @columns = nil @columns_hash = nil @attribute_names = nil + direct_descendants.each do |descendant| + descendant.send(:reload_schema_from_cache) + end end # Guesses the table name, but does not decorate it with prefix and suffix information. diff --git a/activerecord/lib/active_record/persistence.rb b/activerecord/lib/active_record/persistence.rb index 7b53f6e5a0..94316d5249 100644 --- a/activerecord/lib/active_record/persistence.rb +++ b/activerecord/lib/active_record/persistence.rb @@ -1,5 +1,5 @@ module ActiveRecord - # = Active Record Persistence + # = Active Record \Persistence module Persistence extend ActiveSupport::Concern @@ -106,7 +106,7 @@ module ActiveRecord # the existing record gets updated. # # By default, save always run validations. If any of them fail the action - # is cancelled and +save+ returns +false+. However, if you supply + # is cancelled and #save returns +false+. However, if you supply # validate: false, validations are bypassed altogether. See # ActiveRecord::Validations for more information. # @@ -132,7 +132,7 @@ module ActiveRecord # If the model is new, a record gets created in the database, otherwise # the existing record gets updated. # - # With <tt>save!</tt> validations always run. If any of them fail + # With #save! validations always run. If any of them fail # ActiveRecord::RecordInvalid gets raised. See ActiveRecord::Validations # for more information. # @@ -158,7 +158,7 @@ module ActiveRecord # The row is simply removed with an SQL +DELETE+ statement on the # record's primary key, and no callbacks are executed. # - # Note that this will also delete records marked as <tt>readonly?</tt>. + # Note that this will also delete records marked as {#readonly?}[rdoc-ref:Core#readonly?]. # # To enforce the object's +before_destroy+ and +after_destroy+ # callbacks or any <tt>:dependent</tt> association @@ -207,11 +207,11 @@ module ActiveRecord # Note: The new instance will share a link to the same attributes as the original class. # Therefore the sti column value will still be the same. # Any change to the attributes on either instance will affect both instances. - # If you want to change the sti column as well, use +becomes!+ instead. + # If you want to change the sti column as well, use #becomes! instead. def becomes(klass) became = klass.new became.instance_variable_set("@attributes", @attributes) - became.instance_variable_set("@mutation_tracker", @mutation_tracker) + became.instance_variable_set("@mutation_tracker", @mutation_tracker) if defined?(@mutation_tracker) became.instance_variable_set("@changed_attributes", attributes_changed_by_setter) became.instance_variable_set("@new_record", new_record?) became.instance_variable_set("@destroyed", destroyed?) @@ -219,7 +219,7 @@ module ActiveRecord became end - # Wrapper around +becomes+ that also changes the instance's sti column value. + # Wrapper around #becomes that also changes the instance's sti column value. # This is especially useful if you want to persist the changed class in your # database. # @@ -239,14 +239,14 @@ module ActiveRecord # This is especially useful for boolean flags on existing records. Also note that # # * Validation is skipped. - # * Callbacks are invoked. + # * \Callbacks are invoked. # * updated_at/updated_on column is updated if that column is available. # * Updates all the attributes that are dirty in this object. # - # This method raises an +ActiveRecord::ActiveRecordError+ if the + # This method raises an ActiveRecord::ActiveRecordError if the # attribute is marked as readonly. # - # See also +update_column+. + # See also #update_column. def update_attribute(name, value) name = name.to_s verify_readonly_attribute(name) @@ -268,7 +268,7 @@ module ActiveRecord alias update_attributes update - # Updates its receiver just like +update+ but calls <tt>save!</tt> instead + # Updates its receiver just like #update but calls #save! instead # of +save+, so an exception is raised if the record is invalid. def update!(attributes) # The following transaction covers any possible database side-effects of the @@ -295,11 +295,11 @@ module ActiveRecord # the database, but take into account that in consequence the regular update # procedures are totally bypassed. In particular: # - # * Validations are skipped. - # * Callbacks are skipped. + # * \Validations are skipped. + # * \Callbacks are skipped. # * +updated_at+/+updated_on+ are not updated. # - # This method raises an +ActiveRecord::ActiveRecordError+ when called on new + # This method raises an ActiveRecord::ActiveRecordError when called on new # objects, or when at least one of the attributes is marked as readonly. def update_columns(attributes) raise ActiveRecordError, "cannot update a new record" if new_record? @@ -327,29 +327,31 @@ module ActiveRecord self end - # Wrapper around +increment+ that saves the record. This method differs from + # Wrapper around #increment that saves the record. This method differs from # its non-bang version in that it passes through the attribute setter. # Saving is not subjected to validation checks. Returns +true+ if the # record could be saved. def increment!(attribute, by = 1) - increment(attribute, by).update_attribute(attribute, self[attribute]) + increment(attribute, by) + change = public_send(attribute) - (attribute_was(attribute.to_s) || 0) + self.class.update_counters(id, attribute => change) + clear_attribute_change(attribute) # eww + self end # Initializes +attribute+ to zero if +nil+ and subtracts the value passed as +by+ (default is 1). # The decrement is performed directly on the underlying attribute, no setter is invoked. # Only makes sense for number-based attributes. Returns +self+. def decrement(attribute, by = 1) - self[attribute] ||= 0 - self[attribute] -= by - self + increment(attribute, -by) end - # Wrapper around +decrement+ that saves the record. This method differs from + # Wrapper around #decrement that saves the record. This method differs from # its non-bang version in that it passes through the attribute setter. # Saving is not subjected to validation checks. Returns +true+ if the # record could be saved. def decrement!(attribute, by = 1) - decrement(attribute, by).update_attribute(attribute, self[attribute]) + increment!(attribute, -by) end # Assigns to +attribute+ the boolean opposite of <tt>attribute?</tt>. So @@ -361,7 +363,7 @@ module ActiveRecord self end - # Wrapper around +toggle+ that saves the record. This method differs from + # Wrapper around #toggle that saves the record. This method differs from # its non-bang version in that it passes through the attribute setter. # Saving is not subjected to validation checks. Returns +true+ if the # record could be saved. @@ -384,7 +386,7 @@ module ActiveRecord # Attributes are reloaded from the database, and caches busted, in # particular the associations cache and the QueryCache. # - # If the record no longer exists in the database <tt>ActiveRecord::RecordNotFound</tt> + # If the record no longer exists in the database ActiveRecord::RecordNotFound # is raised. Otherwise, in addition to the in-place modification the method # returns +self+ for convenience. # @@ -446,8 +448,8 @@ module ActiveRecord # product.touch(:designed_at) # updates the designed_at attribute and updated_at/on # product.touch(:started_at, :ended_at) # updates started_at, ended_at and updated_at/on attributes # - # If used along with +belongs_to+ then +touch+ will invoke +touch+ method on - # associated object. + # If used along with {belongs_to}[rdoc-ref:Associations::ClassMethods#belongs_to] + # then +touch+ will invoke +touch+ method on associated object. # # class Brake < ActiveRecord::Base # belongs_to :car, touch: true diff --git a/activerecord/lib/active_record/querying.rb b/activerecord/lib/active_record/querying.rb index 87a1988f2f..1f429cfd94 100644 --- a/activerecord/lib/active_record/querying.rb +++ b/activerecord/lib/active_record/querying.rb @@ -7,7 +7,7 @@ module ActiveRecord 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, :in_batches, to: :all - delegate :select, :group, :order, :except, :reorder, :limit, :offset, :joins, :or, + delegate :select, :group, :order, :except, :reorder, :limit, :offset, :joins, :left_joins, :left_outer_joins, :or, :where, :rewhere, :preload, :eager_load, :includes, :from, :lock, :readonly, :having, :create_with, :uniq, :distinct, :references, :none, :unscope, to: :all delegate :count, :average, :minimum, :maximum, :sum, :calculate, to: :all diff --git a/activerecord/lib/active_record/railtie.rb b/activerecord/lib/active_record/railtie.rb index 6dd54f9262..2744673c12 100644 --- a/activerecord/lib/active_record/railtie.rb +++ b/activerecord/lib/active_record/railtie.rb @@ -121,7 +121,7 @@ module ActiveRecord # This sets the database configuration from Configuration#database_configuration # and then establishes the connection. - initializer "active_record.initialize_database" do |app| + initializer "active_record.initialize_database" do ActiveSupport.on_load(:active_record) do self.configurations = Rails.application.config.database_configuration diff --git a/activerecord/lib/active_record/railties/databases.rake b/activerecord/lib/active_record/railties/databases.rake index d940ac244a..5b1ea16f0b 100644 --- a/activerecord/lib/active_record/railties/databases.rake +++ b/activerecord/lib/active_record/railties/databases.rake @@ -34,7 +34,7 @@ db_namespace = namespace :db do end end - # desc "Empty the database from DATABASE_URL or config/database.yml for the current RAILS_ENV (use db:drop:all to drop all databases in the config). Without RAILS_ENV it defaults to purging the development and test databases." + # desc "Empty the database from DATABASE_URL or config/database.yml for the current RAILS_ENV (use db:purge:all to purge all databases in the config). Without RAILS_ENV it defaults to purging the development and test databases." task :purge => [:load_config] do ActiveRecord::Tasks::DatabaseTasks.purge_current end @@ -100,12 +100,14 @@ db_namespace = namespace :db do file_list = ActiveRecord::Tasks::DatabaseTasks.migrations_paths.flat_map do |path| - # match "20091231235959_some_name.rb" and "001_some_name.rb" pattern - Dir.foreach(path).grep(/^(\d{3,})_(.+)\.rb$/) do - version = ActiveRecord::SchemaMigration.normalize_migration_number($1) + Dir.foreach(path).map do |file| + next unless ActiveRecord::Migrator.match_to_migration_filename?(file) + + version, name, scope = ActiveRecord::Migrator.parse_migration_filename(file) + version = ActiveRecord::SchemaMigration.normalize_migration_number(version) status = db_list.delete(version) ? 'up' : 'down' - [status, version, $2.humanize] - end + [status, version, (name + scope).humanize] + end.compact end db_list.map! do |version| diff --git a/activerecord/lib/active_record/reflection.rb b/activerecord/lib/active_record/reflection.rb index 65ec356735..5b9d45d871 100644 --- a/activerecord/lib/active_record/reflection.rb +++ b/activerecord/lib/active_record/reflection.rb @@ -62,7 +62,7 @@ module ActiveRecord aggregate_reflections[aggregation.to_s] end - # Returns a Hash of name of the reflection as the key and a AssociationReflection as the value. + # Returns a Hash of name of the reflection as the key and an AssociationReflection as the value. # # Account.reflections # => {"balance" => AggregateReflection} # diff --git a/activerecord/lib/active_record/relation.rb b/activerecord/lib/active_record/relation.rb index e596df8742..f100476374 100644 --- a/activerecord/lib/active_record/relation.rb +++ b/activerecord/lib/active_record/relation.rb @@ -1,10 +1,10 @@ require "arel/collectors/bind" module ActiveRecord - # = Active Record Relation + # = Active Record \Relation class Relation MULTI_VALUE_METHODS = [:includes, :eager_load, :preload, :select, :group, - :order, :joins, :references, + :order, :joins, :left_joins, :left_outer_joins, :references, :extending, :unscope] SINGLE_VALUE_METHODS = [:limit, :offset, :lock, :readonly, :reordering, @@ -108,7 +108,7 @@ module ActiveRecord # Initializes new record from relation while maintaining the current # scope. # - # Expects arguments in the same format as +Base.new+. + # Expects arguments in the same format as {ActiveRecord::Base.new}[rdoc-ref:Core.new]. # # users = User.where(name: 'DHH') # user = users.new # => #<User id: nil, name: "DHH", created_at: nil, updated_at: nil> @@ -126,28 +126,32 @@ module ActiveRecord # 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+. + # Expects arguments in the same format as + # {ActiveRecord::Base.create}[rdoc-ref:Persistence::ClassMethods#create]. # # ==== Examples + # # users = User.where(name: 'Oscar') - # users.create # #<User id: 3, name: "oscar", ...> + # users.create # => #<User id: 3, name: "oscar", ...> # # users.create(name: 'fxn') - # users.create # #<User id: 4, name: "fxn", ...> + # users.create # => #<User id: 4, name: "fxn", ...> # # users.create { |user| user.name = 'tenderlove' } - # # #<User id: 5, name: "tenderlove", ...> + # # => #<User id: 5, name: "tenderlove", ...> # # users.create(name: nil) # validation on name - # # #<User id: nil, name: nil, ...> + # # => #<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. + # Similar to #create, but calls + # {create!}[rdoc-ref:Persistence::ClassMethods#create!] + # on the base class. Raises an exception if a validation error occurs. # - # Expects arguments in the same format as <tt>Base.create!</tt>. + # Expects arguments in the same format as + # {ActiveRecord::Base.create!}[rdoc-ref:Persistence::ClassMethods#create!]. def create!(*args, &block) scoping { @klass.create!(*args, &block) } end @@ -181,7 +185,7 @@ module ActiveRecord # User.create_with(last_name: 'Johansson').find_or_create_by(first_name: 'Scarlett') # # => #<User id: 2, first_name: "Scarlett", last_name: "Johansson"> # - # This method accepts a block, which is passed down to +create+. The last example + # This method accepts a block, which is passed down to #create. The last example # above can be alternatively written this way: # # # Find the first user named "Scarlett" or create a new one with a @@ -193,7 +197,7 @@ module ActiveRecord # # This method always returns a record, but if creation was attempted and # failed due to validation errors it won't be persisted, you get what - # +create+ returns in such situation. + # #create returns in such situation. # # Please note *this method is not atomic*, it runs first a SELECT, and if # there are no results an INSERT is attempted. If there are other threads @@ -216,13 +220,15 @@ module ActiveRecord find_by(attributes) || create(attributes, &block) end - # Like <tt>find_or_create_by</tt>, but calls <tt>create!</tt> so an exception + # Like #find_or_create_by, but calls + # {create!}[rdoc-ref:Persistence::ClassMethods#create!] so an exception # is raised if the created record is invalid. def find_or_create_by!(attributes, &block) find_by(attributes) || create!(attributes, &block) end - # Like <tt>find_or_create_by</tt>, but calls <tt>new</tt> instead of <tt>create</tt>. + # Like #find_or_create_by, but calls {new}[rdoc-ref:Core#new] + # instead of {create}[rdoc-ref:Persistence::ClassMethods#create]. def find_or_initialize_by(attributes, &block) find_by(attributes) || new(attributes, &block) end @@ -304,7 +310,7 @@ module ActiveRecord # the existing records is updated or deleted, the cache key changes. # # Product.where("name like ?", "%Cosmic Encounter%").cache_key - # => "products/query-1850ab3d302391b85b8693e941286659-1-20150714212553907087000" + # # => "products/query-1850ab3d302391b85b8693e941286659-1-20150714212553907087000" # # If the collection is loaded, the method will iterate through the records # to generate the timestamp, otherwise it will trigger one SQL query like: @@ -341,8 +347,8 @@ module ActiveRecord # Updates all records in the current relation with details given. This method constructs a single SQL UPDATE # statement and sends it straight to the database. It does not instantiate the involved models and it does not - # trigger Active Record callbacks or validations. Values passed to `update_all` will not go through - # ActiveRecord's type-casting behavior. It should receive only values that can be passed as-is to the SQL + # trigger Active Record callbacks or validations. Values passed to #update_all will not go through + # Active Record's type-casting behavior. It should receive only values that can be passed as-is to the SQL # database. # # ==== Parameters @@ -400,17 +406,24 @@ module ActiveRecord # people = Person.where(group: 'expert') # people.update(group: 'masters') # - # Note: Updating a large number of records will run a - # UPDATE query for each record, which may cause a performance - # issue. So if it is not needed to run callbacks for each update, it is - # preferred to use <tt>update_all</tt> for updating all records using - # a single query. + # Note: Updating a large number of records will run an + # UPDATE query for each record, which may cause a performance + # issue. So if it is not needed to run callbacks for each update, it is + # preferred to use #update_all for updating all records using + # a single query. def update(id = :all, attributes) if id.is_a?(Array) id.map.with_index { |one_id, idx| update(one_id, attributes[idx]) } elsif id == :all to_a.each { |record| record.update(attributes) } else + if ActiveRecord::Base === id + id = id.id + ActiveSupport::Deprecation.warn(<<-MSG.squish) + You are passing an instance of ActiveRecord::Base to `update`. + Please pass the id of the object by calling `.id` + MSG + end object = find(id) object.update(attributes) object @@ -418,9 +431,9 @@ module ActiveRecord end # Destroys the records by instantiating each - # record and calling its +destroy+ method. Each object's callbacks are - # executed (including <tt>:dependent</tt> association options). Returns the - # collection of objects that were destroyed; each will be frozen, to + # record and calling its {#destroy}[rdoc-ref:Persistence#destroy] method. + # Each object's callbacks are executed (including <tt>:dependent</tt> association options). + # Returns the collection of objects that were destroyed; each will be frozen, to # reflect that no changes should be made (since they can't be persisted). # # Note: Instantiation, callback execution, and deletion of each @@ -428,7 +441,7 @@ module ActiveRecord # once. It generates at least one SQL +DELETE+ query per record (or # possibly more, to enforce your callbacks). If you want to delete many # rows quickly, without concern for their associations or callbacks, use - # +delete_all+ instead. + # #delete_all instead. # # ==== Examples # @@ -447,7 +460,7 @@ module ActiveRecord # Destroy an object (or multiple objects) that has the given id. The object is instantiated first, # therefore all callbacks and filters are fired off before the object is deleted. This method is - # less efficient than ActiveRecord#delete but allows cleanup methods and other actions to be run. + # less efficient than #delete but allows cleanup methods and other actions to be run. # # This essentially finds the object (or multiple objects) with the given id, creates a new object # from the attributes, and then calls destroy on it. @@ -473,9 +486,10 @@ module ActiveRecord end # Deletes the records 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 + # first, and hence not calling the {#destroy}[rdoc-ref:Persistence#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. # @@ -483,9 +497,9 @@ 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. + # +after_destroy+ callbacks, use the #destroy_all method instead. # - # If an invalid method is supplied, +delete_all+ raises an ActiveRecord error: + # If an invalid method is supplied, #delete_all raises an ActiveRecordError: # # Post.limit(100).delete_all # # => ActiveRecord::ActiveRecordError: delete_all doesn't support limit @@ -534,7 +548,7 @@ module ActiveRecord # You can delete multiple rows at once by passing an Array of <tt>id</tt>s. # # Note: Although it is often much faster than the alternative, - # <tt>#destroy</tt>, skipping callbacks might bypass business logic in + # #destroy, skipping callbacks might bypass business logic in # your application that ensures referential integrity or performs other # essential jobs. # @@ -624,8 +638,10 @@ module ActiveRecord includes_values & joins_values end - # +uniq+ and +uniq!+ are silently deprecated. +uniq_value+ delegates to +distinct_value+ - # to maintain backwards compatibility. Use +distinct_value+ instead. + # {#uniq}[rdoc-ref:QueryMethods#uniq] and + # {#uniq!}[rdoc-ref:QueryMethods#uniq!] are silently deprecated. + # #uniq_value delegates to #distinct_value to maintain backwards compatibility. + # Use #distinct_value instead. def uniq_value distinct_value end diff --git a/activerecord/lib/active_record/relation/batches.rb b/activerecord/lib/active_record/relation/batches.rb index beb8fa511c..221bc73680 100644 --- a/activerecord/lib/active_record/relation/batches.rb +++ b/activerecord/lib/active_record/relation/batches.rb @@ -3,8 +3,8 @@ require "active_record/relation/batches/batch_enumerator" module ActiveRecord module Batches # 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. + # (using the Scoping::Named::ClassMethods.all method, for example) + # is very inefficient since it will try to instantiate all the objects at once. # # In that case, batch processing methods allow you to work # with the records in batches, thereby greatly reducing memory consumption. diff --git a/activerecord/lib/active_record/relation/calculations.rb b/activerecord/lib/active_record/relation/calculations.rb index 69f39e5ba9..f45844a9ea 100644 --- a/activerecord/lib/active_record/relation/calculations.rb +++ b/activerecord/lib/active_record/relation/calculations.rb @@ -14,33 +14,34 @@ module ActiveRecord # Person.distinct.count(:age) # # => counts the number of different age values # - # If +count+ is used with +group+, it returns a Hash whose keys represent the aggregated column, + # If #count is used with {Relation#group}[rdoc-ref:QueryMethods#group], + # it returns a Hash whose keys represent the aggregated column, # and the values are the respective amounts: # # Person.group(:city).count # # => { 'Rome' => 5, 'Paris' => 3 } # - # If +count+ is used with +group+ for multiple columns, it returns a Hash whose + # If #count is used with {Relation#group}[rdoc-ref:QueryMethods#group] for multiple columns, it returns a Hash whose # keys are an array containing the individual values of each column and the value - # of each key would be the +count+. + # of each key would be the #count. # # Article.group(:status, :category).count # # => {["draft", "business"]=>10, ["draft", "technology"]=>4, # ["published", "business"]=>0, ["published", "technology"]=>2} # - # If +count+ is used with +select+, it will count the selected columns: + # If #count is used with {Relation#select}[rdoc-ref:QueryMethods#select], it will count the selected columns: # # Person.select(:age).count # # => counts the number of different age values # - # Note: not all valid +select+ expressions are valid +count+ expressions. The specifics differ + # Note: not all valid {Relation#select}[rdoc-ref:QueryMethods#select] expressions are valid #count expressions. The specifics differ # between databases. In invalid cases, an error from the database is thrown. def count(column_name = nil) calculate(:count, column_name) end # Calculates the average value on a given column. Returns +nil+ if there's - # no row. See +calculate+ for examples with options. + # no row. See #calculate for examples with options. # # Person.average(:age) # => 35.8 def average(column_name) @@ -49,7 +50,7 @@ module ActiveRecord # Calculates the minimum value on a given column. The value is returned # with the same data type of the column, or +nil+ if there's no row. See - # +calculate+ for examples with options. + # #calculate for examples with options. # # Person.minimum(:age) # => 7 def minimum(column_name) @@ -58,7 +59,7 @@ module ActiveRecord # Calculates the maximum value on a given column. The value is returned # with the same data type of the column, or +nil+ if there's no row. See - # +calculate+ for examples with options. + # #calculate for examples with options. # # Person.maximum(:age) # => 93 def maximum(column_name) @@ -66,8 +67,8 @@ module ActiveRecord end # Calculates the sum of values on a given column. The value is returned - # with the same data type of the column, 0 if there's no row. See - # +calculate+ for examples with options. + # with the same data type of the column, +0+ if there's no row. See + # #calculate for examples with options. # # Person.sum(:age) # => 4562 def sum(column_name = nil, &block) @@ -75,37 +76,37 @@ module ActiveRecord calculate(:sum, column_name) end - # This calculates aggregate values in the given column. Methods for count, sum, average, - # minimum, and maximum have been added as shortcuts. + # This calculates aggregate values in the given column. Methods for #count, #sum, #average, + # #minimum, and #maximum have been added as shortcuts. # - # There are two basic forms of output: + # Person.calculate(:count, :all) # The same as Person.count + # Person.average(:age) # SELECT AVG(age) FROM people... # - # * 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. + # # Selects the minimum age for any family without any minors + # Person.group(:last_name).having("min(age) > 17").minimum(:age) # - # * 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. + # Person.sum("2 * age") # - # values = Person.group('last_name').maximum(:age) - # puts values["Drake"] - # # => 43 + # There are two basic forms of output: # - # drake = Family.find_by(last_name: 'Drake') - # values = Person.group(:family).maximum(:age) # Person belongs_to :family - # puts values[drake] - # # => 43 + # * 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. # - # values.each do |family, max_age| - # ... - # end + # * 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. # - # Person.calculate(:count, :all) # The same as Person.count - # Person.average(:age) # SELECT AVG(age) FROM people... + # values = Person.group('last_name').maximum(:age) + # puts values["Drake"] + # # => 43 # - # # Selects the minimum age for any family without any minors - # Person.group(:last_name).having("min(age) > 17").minimum(:age) + # drake = Family.find_by(last_name: 'Drake') + # values = Person.group(:family).maximum(:age) # Person belongs_to :family + # puts values[drake] + # # => 43 # - # Person.sum("2 * age") + # values.each do |family, max_age| + # ... + # end def calculate(operation, column_name) if column_name.is_a?(Symbol) && attribute_alias?(column_name) column_name = attribute_alias(column_name) @@ -118,7 +119,7 @@ module ActiveRecord end end - # Use <tt>pluck</tt> as a shortcut to select one or more attributes without + # Use #pluck as a shortcut to select one or more attributes without # loading a bunch of records just to grab the attributes you want. # # Person.pluck(:name) @@ -127,7 +128,7 @@ module ActiveRecord # # Person.all.map(&:name) # - # Pluck returns an <tt>Array</tt> of attribute values type-casted to match + # Pluck returns an Array of attribute values type-casted to match # the plucked column names, if they can be deduced. Plucking an SQL fragment # returns String values by default. # @@ -151,7 +152,7 @@ module ActiveRecord # # SELECT DATEDIFF(updated_at, created_at) FROM people # # => ['0', '27761', '173'] # - # See also +ids+. + # See also #ids. # def pluck(*column_names) column_names.map! do |column_name| @@ -218,6 +219,8 @@ module ActiveRecord end def aggregate_column(column_name) + return column_name if Arel::Expressions === column_name + if @klass.column_names.include?(column_name.to_s) Arel::Attribute.new(@klass.unscoped.table, column_name) else @@ -272,15 +275,10 @@ module ActiveRecord else group_fields = group_attrs end + group_fields = arel_columns(group_fields) - group_aliases = group_fields.map { |field| - column_alias_for(field) - } - group_columns = group_aliases.zip(group_fields).map { |aliaz,field| - [aliaz, field] - } - - group = group_fields + group_aliases = group_fields.map { |field| column_alias_for(field) } + group_columns = group_aliases.zip(group_fields) if operation == 'count' && column_name == :all aggregate_alias = 'count_all' @@ -296,7 +294,7 @@ module ActiveRecord ] select_values += select_values unless having_clause.empty? - select_values.concat group_fields.zip(group_aliases).map { |field,aliaz| + select_values.concat group_columns.map { |aliaz, field| if field.respond_to?(:as) field.as(aliaz) else @@ -305,7 +303,7 @@ module ActiveRecord } relation = except(:group) - relation.group_values = group + relation.group_values = group_fields relation.select_values = select_values calculated_data = @klass.connection.select_all(relation, nil, relation.bound_attributes) @@ -366,9 +364,9 @@ module ActiveRecord end end - # TODO: refactor to allow non-string `select_values` (eg. Arel nodes). def select_for_count if select_values.present? + return select_values.first if select_values.one? select_values.join(", ") else :all diff --git a/activerecord/lib/active_record/relation/delegation.rb b/activerecord/lib/active_record/relation/delegation.rb index d75ec72b1a..27de313d05 100644 --- a/activerecord/lib/active_record/relation/delegation.rb +++ b/activerecord/lib/active_record/relation/delegation.rb @@ -3,12 +3,12 @@ require 'active_support/concern' module ActiveRecord module Delegation # :nodoc: - module DelegateCache - def relation_delegate_class(klass) # :nodoc: + module DelegateCache # :nodoc: + def relation_delegate_class(klass) @relation_delegate_cache[klass] end - def initialize_relation_delegate_cache # :nodoc: + def initialize_relation_delegate_cache @relation_delegate_cache = cache = {} [ ActiveRecord::Relation, diff --git a/activerecord/lib/active_record/relation/finder_methods.rb b/activerecord/lib/active_record/relation/finder_methods.rb index 009b2bad57..435cef901b 100644 --- a/activerecord/lib/active_record/relation/finder_methods.rb +++ b/activerecord/lib/active_record/relation/finder_methods.rb @@ -17,7 +17,7 @@ module ActiveRecord # Person.where("administrator = 1").order("created_on DESC").find(1) # # NOTE: The returned records may not be in the same order as the ids you - # provide since database rows are unordered. You'd need to provide an explicit <tt>order</tt> + # provide since database rows are unordered. You'd need to provide an explicit QueryMethods#order # option if you want the results are sorted. # # ==== Find with lock @@ -34,7 +34,7 @@ module ActiveRecord # person.save! # end # - # ==== Variations of +find+ + # ==== Variations of #find # # Person.where(name: 'Spartacus', rating: 4) # # returns a chainable list (which can be empty). @@ -48,7 +48,7 @@ module ActiveRecord # Person.where(name: 'Spartacus', rating: 4).first_or_create # # returns the first item or creates it and returns it. # - # ==== Alternatives for +find+ + # ==== Alternatives for #find # # Person.where(name: 'Spartacus', rating: 4).exists?(conditions = :none) # # returns a boolean indicating if any record with the given conditions exist. @@ -80,8 +80,8 @@ module ActiveRecord nil end - # Like <tt>find_by</tt>, except that if no record is found, raises - # an <tt>ActiveRecord::RecordNotFound</tt> error. + # Like #find_by, except that if no record is found, raises + # an ActiveRecord::RecordNotFound error. def find_by!(arg, *args) where(arg, *args).take! rescue RangeError @@ -100,8 +100,8 @@ module ActiveRecord 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. + # Same as #take but raises ActiveRecord::RecordNotFound if no record + # is found. Note that #take! accepts no arguments. def take! take or raise RecordNotFound.new("Couldn't find #{@klass.name} with [#{arel.where_sql(@klass.arel_engine)}]") end @@ -123,8 +123,8 @@ module ActiveRecord end end - # Same as +first+ but raises <tt>ActiveRecord::RecordNotFound</tt> if no record - # is found. Note that <tt>first!</tt> accepts no arguments. + # Same as #first but raises ActiveRecord::RecordNotFound if no record + # is found. Note that #first! accepts no arguments. def first! find_nth! 0 end @@ -156,8 +156,8 @@ module ActiveRecord end end - # Same as +last+ but raises <tt>ActiveRecord::RecordNotFound</tt> if no record - # is found. Note that <tt>last!</tt> accepts no arguments. + # Same as #last but raises ActiveRecord::RecordNotFound if no record + # is found. Note that #last! accepts no arguments. def last! last or raise RecordNotFound.new("Couldn't find #{@klass.name} with [#{arel.where_sql(@klass.arel_engine)}]") end @@ -172,7 +172,7 @@ module ActiveRecord find_nth(1, offset_index) end - # Same as +second+ but raises <tt>ActiveRecord::RecordNotFound</tt> if no record + # Same as #second but raises ActiveRecord::RecordNotFound if no record # is found. def second! find_nth! 1 @@ -188,7 +188,7 @@ module ActiveRecord find_nth(2, offset_index) end - # Same as +third+ but raises <tt>ActiveRecord::RecordNotFound</tt> if no record + # Same as #third but raises ActiveRecord::RecordNotFound if no record # is found. def third! find_nth! 2 @@ -204,7 +204,7 @@ module ActiveRecord find_nth(3, offset_index) end - # Same as +fourth+ but raises <tt>ActiveRecord::RecordNotFound</tt> if no record + # Same as #fourth but raises ActiveRecord::RecordNotFound if no record # is found. def fourth! find_nth! 3 @@ -220,7 +220,7 @@ module ActiveRecord find_nth(4, offset_index) end - # Same as +fifth+ but raises <tt>ActiveRecord::RecordNotFound</tt> if no record + # Same as #fifth but raises ActiveRecord::RecordNotFound if no record # is found. def fifth! find_nth! 4 @@ -236,14 +236,14 @@ module ActiveRecord find_nth(41, offset_index) end - # Same as +forty_two+ but raises <tt>ActiveRecord::RecordNotFound</tt> if no record + # Same as #forty_two but raises ActiveRecord::RecordNotFound if no record # is found. def forty_two! find_nth! 41 end - # 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: + # 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 @@ -256,7 +256,7 @@ module ActiveRecord # * 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 <tt>ActiveRecord::Base</tt>. + # see the Conditions section in the introduction to ActiveRecord::Base. # # Note: You can't pass in a condition as a string (like <tt>name = # 'Jamie'</tt>), since it would be sanitized and then queried against @@ -298,7 +298,7 @@ module ActiveRecord end # This method is called whenever no records are found with either a single - # id or multiple ids and raises a +ActiveRecord::RecordNotFound+ exception. + # id or multiple ids and raises a ActiveRecord::RecordNotFound exception. # # The error message is different depending on whether a single id or # multiple ids are provided. If multiple ids are provided, then the number diff --git a/activerecord/lib/active_record/relation/from_clause.rb b/activerecord/lib/active_record/relation/from_clause.rb index a93952fa30..92340216ed 100644 --- a/activerecord/lib/active_record/relation/from_clause.rb +++ b/activerecord/lib/active_record/relation/from_clause.rb @@ -1,6 +1,6 @@ module ActiveRecord class Relation - class FromClause + class FromClause # :nodoc: attr_reader :value, :name def initialize(value, name) diff --git a/activerecord/lib/active_record/relation/query_attribute.rb b/activerecord/lib/active_record/relation/query_attribute.rb index e69319b4de..7ba964e802 100644 --- a/activerecord/lib/active_record/relation/query_attribute.rb +++ b/activerecord/lib/active_record/relation/query_attribute.rb @@ -2,7 +2,7 @@ require 'active_record/attribute' module ActiveRecord class Relation - class QueryAttribute < Attribute + class QueryAttribute < Attribute # :nodoc: def type_cast(value) value end diff --git a/activerecord/lib/active_record/relation/query_methods.rb b/activerecord/lib/active_record/relation/query_methods.rb index eb53a18f0f..2dc52982c9 100644 --- a/activerecord/lib/active_record/relation/query_methods.rb +++ b/activerecord/lib/active_record/relation/query_methods.rb @@ -3,7 +3,6 @@ require "active_record/relation/query_attribute" require "active_record/relation/where_clause" require "active_record/relation/where_clause_factory" require 'active_model/forbidden_attributes_protection' -require 'active_support/core_ext/string/filters' module ActiveRecord module QueryMethods @@ -21,7 +20,7 @@ module ActiveRecord # Returns a new relation expressing WHERE + NOT condition according to # the conditions in the arguments. # - # +not+ accepts conditions as a string, array, or hash. See #where for + # #not accepts conditions as a string, array, or hash. See QueryMethods#where for # more details on each format. # # User.where.not("name = 'Jon'") @@ -113,7 +112,7 @@ module ActiveRecord # # 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+. + # performance improvement over a simple join. # # You can also specify multiple relationships, like this: # @@ -134,7 +133,7 @@ module ActiveRecord # # User.includes(:posts).where('posts.name = ?', 'example').references(:posts) # - # Note that +includes+ works with association names while +references+ needs + # Note that #includes works with association names while #references needs # the actual table name. def includes(*args) check_if_method_has_arguments!(:includes, args) @@ -152,9 +151,9 @@ module ActiveRecord # 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" + # # 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) check_if_method_has_arguments!(:eager_load, args) spawn.eager_load!(*args) @@ -165,10 +164,10 @@ module ActiveRecord self end - # Allows preloading of +args+, in the same way that +includes+ does: + # 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) + # # SELECT "posts".* FROM "posts" WHERE "posts"."user_id" IN (1, 2, 3) def preload(*args) check_if_method_has_arguments!(:preload, args) spawn.preload!(*args) @@ -181,14 +180,14 @@ module ActiveRecord # Use to indicate that the given +table_names+ are referenced by an SQL string, # and should therefore be JOINed in any query rather than loaded separately. - # This method only works in conjunction with +includes+. + # This method only works in conjunction with #includes. # See #includes for more details. # # User.includes(:posts).where("posts.name = 'foo'") - # # => Doesn't JOIN the posts table, resulting in an error. + # # 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 + # # Query now knows the string references posts, so adds a JOIN def references(*table_names) check_if_method_has_arguments!(:references, table_names) spawn.references!(*table_names) @@ -204,12 +203,12 @@ module ActiveRecord # Works in two unique ways. # - # First: takes a block so it can be used just like Array#select. + # First: takes a block so it can be used just like +Array#select+. # # 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. + # converting them into an array and iterating through them using +Array#select+. # # Second: Modifies the SELECT statement for the query so that only certain # fields are retrieved: @@ -237,7 +236,7 @@ module ActiveRecord # # => "value" # # Accessing attributes of an object that do not have fields retrieved by a select - # except +id+ will throw <tt>ActiveModel::MissingAttributeError</tt>: + # except +id+ will throw ActiveModel::MissingAttributeError: # # Model.select(:field).first.other_field # # => ActiveModel::MissingAttributeError: missing attribute: other_field @@ -259,22 +258,23 @@ module ActiveRecord # Allows to specify a group attribute: # # User.group(:name) - # => SELECT "users".* FROM "users" GROUP BY 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 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", ...>] + # # => [#<User id: 3, name: "Foo", ...>, #<User id: 2, name: "Oscar", ...>] # # User.group('name AS grouped_name, age') - # => [#<User id: 3, name: "Foo", age: 21, ...>, #<User id: 2, name: "Oscar", age: 21, ...>, #<User id: 5, name: "Foo", age: 23, ...>] + # # => [#<User id: 3, name: "Foo", age: 21, ...>, #<User id: 2, name: "Oscar", age: 21, ...>, #<User id: 5, name: "Foo", age: 23, ...>] # # Passing in an array of attributes to group by is also supported. + # # User.select([:id, :first_name]).group(:id, :first_name).first(3) - # => [#<User id: 1, first_name: "Bill">, #<User id: 2, first_name: "Earl">, #<User id: 3, first_name: "Beto">] + # # => [#<User id: 1, first_name: "Bill">, #<User id: 2, first_name: "Earl">, #<User id: 3, first_name: "Beto">] def group(*args) check_if_method_has_arguments!(:group, args) spawn.group!(*args) @@ -290,22 +290,22 @@ module ActiveRecord # Allows to specify an order attribute: # # User.order(:name) - # => SELECT "users".* FROM "users" ORDER BY "users"."name" ASC + # # SELECT "users".* FROM "users" ORDER BY "users"."name" ASC # # User.order(email: :desc) - # => SELECT "users".* FROM "users" ORDER BY "users"."email" DESC + # # SELECT "users".* FROM "users" ORDER BY "users"."email" DESC # # User.order(:name, email: :desc) - # => SELECT "users".* FROM "users" ORDER BY "users"."name" ASC, "users"."email" DESC + # # SELECT "users".* FROM "users" ORDER BY "users"."name" ASC, "users"."email" DESC # # User.order('name') - # => SELECT "users".* FROM "users" ORDER BY name + # # SELECT "users".* FROM "users" ORDER BY name # # User.order('name DESC') - # => SELECT "users".* FROM "users" ORDER BY name DESC + # # SELECT "users".* FROM "users" ORDER BY name DESC # # User.order('name DESC, email') - # => SELECT "users".* FROM "users" ORDER BY name DESC, email + # # SELECT "users".* FROM "users" ORDER BY name DESC, email def order(*args) check_if_method_has_arguments!(:order, args) spawn.order!(*args) @@ -357,15 +357,15 @@ module ActiveRecord # User.order('email DESC').select('id').where(name: "John") # .unscope(:order, :select, :where) == User.all # - # One can additionally pass a hash as an argument to unscope specific :where values. + # One can additionally pass a hash as an argument to unscope specific +:where+ values. # This is done by passing a hash with a single key-value pair. The key should be - # :where and the value should be the where value to unscope. For example: + # +:where+ and the value should be the where value to unscope. For example: # # User.where(name: "John", active: true).unscope(where: :name) # == User.where(active: true) # - # This method is similar to <tt>except</tt>, but unlike - # <tt>except</tt>, it persists across merges: + # This method is similar to #except, but unlike + # #except, it persists across merges: # # User.order('email').merge(User.except(:order)) # == User.order('email') @@ -375,7 +375,7 @@ module ActiveRecord # # This means it can be used in association definitions: # - # has_many :comments, -> { unscope where: :trashed } + # has_many :comments, -> { unscope(where: :trashed) } # def unscope(*args) check_if_method_has_arguments!(:unscope, args) @@ -410,12 +410,12 @@ module ActiveRecord # Performs a joins on +args+: # # User.joins(:posts) - # => SELECT "users".* FROM "users" INNER JOIN "posts" ON "posts"."user_id" = "users"."id" + # # SELECT "users".* FROM "users" INNER JOIN "posts" ON "posts"."user_id" = "users"."id" # # You can use strings in order to customize your joins: # # User.joins("LEFT JOIN bookmarks ON bookmarks.bookmarkable_type = 'Post' AND bookmarks.user_id = users.id") - # => SELECT "users".* FROM "users" LEFT JOIN bookmarks ON bookmarks.bookmarkable_type = 'Post' AND bookmarks.user_id = users.id + # # SELECT "users".* FROM "users" LEFT JOIN bookmarks ON bookmarks.bookmarkable_type = 'Post' AND bookmarks.user_id = users.id def joins(*args) check_if_method_has_arguments!(:joins, args) spawn.joins!(*args) @@ -428,6 +428,27 @@ module ActiveRecord self end + # Performs a left outer joins on +args+: + # + # User.left_outer_joins(:posts) + # => SELECT "users".* FROM "users" LEFT OUTER JOIN "posts" ON "posts"."user_id" = "users"."id" + # + def left_outer_joins(*args) + check_if_method_has_arguments!(:left_outer_joins, args) + + args.compact! + args.flatten! + + spawn.left_outer_joins!(*args) + end + alias :left_joins :left_outer_joins + + def left_outer_joins!(*args) # :nodoc: + self.left_outer_joins_values += args + self + end + alias :left_joins! :left_outer_joins! + # Returns a new relation, which is the result of filtering the current relation # according to the conditions in the arguments. # @@ -471,7 +492,7 @@ module ActiveRecord # 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>. + # the values are inserted using the same escapes as the Ruby core method +Kernel::sprintf+. # # User.where(["name = '%s' and email = '%s'", "Joe", "joe@example.com"]) # # SELECT * FROM users WHERE name = 'Joe' AND email = 'joe@example.com'; @@ -558,23 +579,25 @@ module ActiveRecord end def where!(opts, *rest) # :nodoc: - if Hash === opts - opts = sanitize_forbidden_attributes(opts) - references!(PredicateBuilder.references(opts)) - end - + opts = sanitize_forbidden_attributes(opts) + references!(PredicateBuilder.references(opts)) if Hash === opts self.where_clause += where_clause_factory.build(opts, rest) self end # Allows you to change a previously set where condition for a given attribute, instead of appending to that condition. # - # Post.where(trashed: true).where(trashed: false) # => WHERE `trashed` = 1 AND `trashed` = 0 - # Post.where(trashed: true).rewhere(trashed: false) # => WHERE `trashed` = 0 - # Post.where(active: true).where(trashed: true).rewhere(trashed: false) # => WHERE `active` = 1 AND `trashed` = 0 + # Post.where(trashed: true).where(trashed: false) + # # WHERE `trashed` = 1 AND `trashed` = 0 + # + # Post.where(trashed: true).rewhere(trashed: false) + # # WHERE `trashed` = 0 # - # This is short-hand for unscope(where: conditions.keys).where(conditions). Note that unlike reorder, we're only unscoping - # the named conditions -- not the entire where statement. + # Post.where(active: true).where(trashed: true).rewhere(trashed: false) + # # WHERE `active` = 1 AND `trashed` = 0 + # + # This is short-hand for <tt>unscope(where: conditions.keys).where(conditions)</tt>. + # Note that unlike reorder, we're only unscoping the named conditions -- not the entire where statement. def rewhere(conditions) unscope(where: conditions.keys).where(conditions) end @@ -583,8 +606,8 @@ module ActiveRecord # argument. # # The two relations must be structurally compatible: they must be scoping the same model, and - # they must differ only by +where+ (if no +group+ has been defined) or +having+ (if a +group+ is - # present). Neither relation may have a +limit+, +offset+, or +distinct+ set. + # they must differ only by #where (if no #group has been defined) or #having (if a #group is + # present). Neither relation may have a #limit, #offset, or #distinct set. # # Post.where("id = 1").or(Post.where("id = 2")) # # SELECT `posts`.* FROM `posts` WHERE (('id = 1' OR 'id = 2')) @@ -604,12 +627,6 @@ module ActiveRecord self end - private def structurally_compatible_for_or?(other) # :nodoc: - Relation::SINGLE_VALUE_METHODS.all? { |m| send("#{m}_value") == other.send("#{m}_value") } && - (Relation::MULTI_VALUE_METHODS - [:extending]).all? { |m| send("#{m}_values") == other.send("#{m}_values") } && - (Relation::CLAUSE_METHODS - [:having, :where]).all? { |m| send("#{m}_clause") != other.send("#{m}_clause") } - end - # Allows to specify a HAVING clause. Note that you can't use HAVING # without also specifying a GROUP clause. # @@ -619,6 +636,7 @@ module ActiveRecord end def having!(opts, *rest) # :nodoc: + opts = sanitize_forbidden_attributes(opts) references!(PredicateBuilder.references(opts)) if Hash === opts self.having_clause += having_clause_factory.build(opts, rest) @@ -656,7 +674,7 @@ module ActiveRecord end # Specifies locking settings (default to +true+). For more information - # on locking, please see +ActiveRecord::Locking+. + # on locking, please see ActiveRecord::Locking. def lock(locks = true) spawn.lock!(locks) end @@ -687,7 +705,7 @@ module ActiveRecord # For example: # # @posts = current_user.visible_posts.where(name: params[:name]) - # # => the visible_posts method is expected to return a chainable Relation + # # the visible_posts method is expected to return a chainable Relation # # def visible_posts # case role @@ -732,7 +750,7 @@ module ActiveRecord # users = users.create_with(name: 'DHH') # users.new.name # => 'DHH' # - # You can pass +nil+ to +create_with+ to reset attributes: + # You can pass +nil+ to #create_with to reset attributes: # # users = users.create_with(nil) # users.new.name # => 'Oscar' @@ -754,15 +772,15 @@ module ActiveRecord # Specifies table from which the records will be fetched. For example: # # Topic.select('title').from('posts') - # # => 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 + # # 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 + # # SELECT a.title FROM (SELECT * FROM topics WHERE approved = 't') a # def from(value, subquery_name = nil) spawn.from!(value, subquery_name) @@ -776,13 +794,13 @@ module ActiveRecord # Specifies whether the records should be unique or not. For example: # # User.select(:name) - # # => Might return two records with the same name + # # Might return two records with the same name # # User.select(:name).distinct - # # => Returns 1 record per distinct name + # # Returns 1 record per distinct name # # User.select(:name).distinct.distinct(false) - # # => You can also remove the uniqueness + # # You can also remove the uniqueness def distinct(value = true) spawn.distinct!(value) end @@ -881,6 +899,7 @@ module ActiveRecord arel = Arel::SelectManager.new(table) build_joins(arel, joins_values.flatten) unless joins_values.empty? + build_left_outer_joins(arel, left_outer_joins_values.flatten) unless left_outer_joins_values.empty? arel.where(where_clause.ast) unless where_clause.empty? arel.having(having_clause.ast) unless having_clause.empty? @@ -940,6 +959,19 @@ module ActiveRecord end end + def build_left_outer_joins(manager, outer_joins) + buckets = outer_joins.group_by do |join| + case join + when Hash, Symbol, Array + :association_join + else + raise ArgumentError, 'only Hash, Symbol and Array are allowed' + end + end + + build_join_query(manager, buckets, Arel::Nodes::OuterJoin) + end + def build_joins(manager, joins) buckets = joins.group_by do |join| case join @@ -955,6 +987,11 @@ module ActiveRecord raise 'unknown class: %s' % join.class.name end end + + build_join_query(manager, buckets, Arel::Nodes::InnerJoin) + end + + def build_join_query(manager, buckets, join_type) buckets.default = [] association_joins = buckets[:association_join] @@ -970,7 +1007,7 @@ module ActiveRecord join_list ) - join_infos = join_dependency.join_constraints stashed_association_joins + join_infos = join_dependency.join_constraints stashed_association_joins, join_type join_infos.each do |info| info.joins.each { |join| manager.from(join) } @@ -1048,6 +1085,9 @@ module ActiveRecord end def preprocess_order_args(order_args) + order_args.map! do |arg| + klass.send(:sanitize_sql_for_order, arg) + end order_args.flatten! validate_order_args(order_args) @@ -1078,8 +1118,8 @@ module ActiveRecord # # Example: # - # Post.references() # => raises an error - # Post.references([]) # => does not raise an error + # Post.references() # raises an error + # Post.references([]) # does not raise an error # # This particular method should be called with a method_name and the args # passed into that method as an input. For example: @@ -1094,6 +1134,12 @@ module ActiveRecord end end + def structurally_compatible_for_or?(other) + Relation::SINGLE_VALUE_METHODS.all? { |m| send("#{m}_value") == other.send("#{m}_value") } && + (Relation::MULTI_VALUE_METHODS - [:extending]).all? { |m| send("#{m}_values") == other.send("#{m}_values") } && + (Relation::CLAUSE_METHODS - [:having, :where]).all? { |m| send("#{m}_clause") != other.send("#{m}_clause") } + end + def new_where_clause Relation::WhereClause.empty end diff --git a/activerecord/lib/active_record/relation/spawn_methods.rb b/activerecord/lib/active_record/relation/spawn_methods.rb index 70da37fa84..5c3318651a 100644 --- a/activerecord/lib/active_record/relation/spawn_methods.rb +++ b/activerecord/lib/active_record/relation/spawn_methods.rb @@ -10,7 +10,7 @@ module ActiveRecord clone end - # Merges in the conditions from <tt>other</tt>, if <tt>other</tt> is an <tt>ActiveRecord::Relation</tt>. + # Merges in the conditions from <tt>other</tt>, if <tt>other</tt> is an ActiveRecord::Relation. # Returns an array representing the intersection of the resulting records with <tt>other</tt>, if <tt>other</tt> is an array. # Post.where(published: true).joins(:comments).merge( Comment.where(spam: false) ) # # Performs a single join query with both where conditions. diff --git a/activerecord/lib/active_record/relation/where_clause_factory.rb b/activerecord/lib/active_record/relation/where_clause_factory.rb index 23eaab4699..a81ff98e49 100644 --- a/activerecord/lib/active_record/relation/where_clause_factory.rb +++ b/activerecord/lib/active_record/relation/where_clause_factory.rb @@ -1,6 +1,6 @@ module ActiveRecord class Relation - class WhereClauseFactory + class WhereClauseFactory # :nodoc: def initialize(klass, predicate_builder) @klass = klass @predicate_builder = predicate_builder @@ -20,8 +20,10 @@ module ActiveRecord attributes, binds = predicate_builder.create_binds(attributes) parts = predicate_builder.build_from_hash(attributes) - else + when Arel::Nodes::Node parts = [opts] + else + raise ArgumentError, "Unsupported argument type: #{opts} (#{opts.class})" end WhereClause.new(parts, binds) diff --git a/activerecord/lib/active_record/result.rb b/activerecord/lib/active_record/result.rb index 500c478e65..8e6cd6c82f 100644 --- a/activerecord/lib/active_record/result.rb +++ b/activerecord/lib/active_record/result.rb @@ -1,7 +1,8 @@ module ActiveRecord ### - # This class encapsulates a Result returned from calling +exec_query+ on any - # database connection adapter. For example: + # This class encapsulates a result returned from calling + # {#exec_query}[rdoc-ref:ConnectionAdapters::DatabaseStatements#exec_query] + # on any database connection adapter. For example: # # result = ActiveRecord::Base.connection.exec_query('SELECT id, title, body FROM posts') # result # => #<ActiveRecord::Result:0xdeadbeef> diff --git a/activerecord/lib/active_record/runtime_registry.rb b/activerecord/lib/active_record/runtime_registry.rb index 9d605b826a..56e88bc661 100644 --- a/activerecord/lib/active_record/runtime_registry.rb +++ b/activerecord/lib/active_record/runtime_registry.rb @@ -7,7 +7,7 @@ module ActiveRecord # # returns the connection handler local to the current thread. # - # See the documentation of <tt>ActiveSupport::PerThreadRegistry</tt> + # See the documentation of ActiveSupport::PerThreadRegistry # for further details. class RuntimeRegistry # :nodoc: extend ActiveSupport::PerThreadRegistry diff --git a/activerecord/lib/active_record/sanitization.rb b/activerecord/lib/active_record/sanitization.rb index 7f6664ea50..0c15f45db9 100644 --- a/activerecord/lib/active_record/sanitization.rb +++ b/activerecord/lib/active_record/sanitization.rb @@ -3,7 +3,8 @@ module ActiveRecord extend ActiveSupport::Concern module ClassMethods - # Used to sanitize objects before they're used in an SQL SELECT statement. Delegates to <tt>connection.quote</tt>. + # Used to sanitize objects before they're used in an SQL SELECT statement. + # Delegates to {connection.quote}[rdoc-ref:ConnectionAdapters::Quoting#quote]. def sanitize(object) # :nodoc: connection.quote(object) end @@ -52,9 +53,25 @@ module ActiveRecord end end + # Accepts an array, or string of SQL conditions and sanitizes + # them into a valid SQL fragment for a ORDER clause. + # + # sanitize_sql_for_order(["field(id, ?)", [1,3,2]]) + # # => "field(id, 1,3,2)" + # + # sanitize_sql_for_order("id ASC") + # # => "id ASC" + def sanitize_sql_for_order(condition) + if condition.is_a?(Array) && condition.first.to_s.include?('?') + sanitize_sql_array(condition) + else + condition + end + end + # Accepts a hash of SQL conditions and replaces those attributes - # that correspond to a +composed_of+ relationship with their expanded - # aggregate attribute values. + # that correspond to a {#composed_of}[rdoc-ref:Aggregations::ClassMethods#composed_of] + # relationship with their expanded aggregate attribute values. # # Given: # diff --git a/activerecord/lib/active_record/schema.rb b/activerecord/lib/active_record/schema.rb index c1a42dc629..31dd584538 100644 --- a/activerecord/lib/active_record/schema.rb +++ b/activerecord/lib/active_record/schema.rb @@ -1,5 +1,5 @@ module ActiveRecord - # = Active Record Schema + # = Active Record \Schema # # Allows programmers to programmatically define a schema in a portable # DSL. This means you can define tables, indexes, etc. without using SQL @@ -30,8 +30,9 @@ module ActiveRecord class Schema < Migration # 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+, - # +add_index+, etc.). + # database definition DSL to build up your schema ( + # {create_table}[rdoc-ref:ConnectionAdapters::SchemaStatements#create_table], + # {add_index}[rdoc-ref:ConnectionAdapters::SchemaStatements#add_index], etc.). # # The +info+ hash is optional, and if given is used to define metadata # about the current schema (currently, only the schema's version): diff --git a/activerecord/lib/active_record/scoping.rb b/activerecord/lib/active_record/scoping.rb index f049b658c4..e395970dc6 100644 --- a/activerecord/lib/active_record/scoping.rb +++ b/activerecord/lib/active_record/scoping.rb @@ -30,7 +30,7 @@ module ActiveRecord end end - def populate_with_current_scope_attributes + def populate_with_current_scope_attributes # :nodoc: return unless self.class.scope_attributes? self.class.scope_attributes.each do |att,value| @@ -38,7 +38,7 @@ module ActiveRecord end end - def initialize_internals_callback + def initialize_internals_callback # :nodoc: super populate_with_current_scope_attributes end @@ -59,8 +59,8 @@ module ActiveRecord # # registry.value_for(:current_scope, "Board") # - # You will obtain whatever was defined in +some_new_scope+. The +value_for+ - # and +set_value_for+ methods are delegated to the current +ScopeRegistry+ + # You will obtain whatever was defined in +some_new_scope+. The #value_for + # and #set_value_for methods are delegated to the current ScopeRegistry # object, so the above example code can also be called as: # # ActiveRecord::Scoping::ScopeRegistry.set_value_for(:current_scope, diff --git a/activerecord/lib/active_record/scoping/default.rb b/activerecord/lib/active_record/scoping/default.rb index fac566e12b..cdcb73382f 100644 --- a/activerecord/lib/active_record/scoping/default.rb +++ b/activerecord/lib/active_record/scoping/default.rb @@ -17,7 +17,7 @@ module ActiveRecord # # class Post < ActiveRecord::Base # def self.default_scope - # where published: true + # where(published: true) # end # end # @@ -55,7 +55,7 @@ module ActiveRecord # # Article.all # => SELECT * FROM articles WHERE published = true # - # The +default_scope+ is also applied while creating/building a record. + # The #default_scope is also applied while creating/building a record. # It is not applied while updating a record. # # Article.new.published # => true @@ -65,7 +65,7 @@ module ActiveRecord # +default_scope+ macro, and it will be called when building the # default scope.) # - # If you use multiple +default_scope+ declarations in your model then + # If you use multiple #default_scope declarations in your model then # they will be merged together: # # class Article < ActiveRecord::Base @@ -76,7 +76,7 @@ module ActiveRecord # Article.all # => SELECT * FROM articles WHERE published = true AND rating = 'G' # # This is also the case with inheritance and module includes where the - # parent or module defines a +default_scope+ and the child or including + # parent or module defines a #default_scope and the child or including # class defines a second one. # # If you need to do more complex things with a default scope, you can diff --git a/activerecord/lib/active_record/scoping/named.rb b/activerecord/lib/active_record/scoping/named.rb index 7b62626896..103569c84d 100644 --- a/activerecord/lib/active_record/scoping/named.rb +++ b/activerecord/lib/active_record/scoping/named.rb @@ -9,7 +9,7 @@ module ActiveRecord extend ActiveSupport::Concern module ClassMethods - # Returns an <tt>ActiveRecord::Relation</tt> scope object. + # Returns an ActiveRecord::Relation scope object. # # posts = Post.all # posts.size # Fires "select count(*) from posts" and returns the count @@ -20,7 +20,7 @@ module ActiveRecord # fruits = fruits.limit(10) if limited? # # You can define a scope that applies to all finders using - # <tt>ActiveRecord::Base.default_scope</tt>. + # {default_scope}[rdoc-ref:Scoping::Default::ClassMethods#default_scope]. def all if current_scope current_scope.clone @@ -39,8 +39,13 @@ module ActiveRecord end end - # Adds a class method for retrieving and querying objects. A \scope - # represents a narrowing of a database query, such as + # Adds a class method for retrieving and querying objects. + # The method is intended to return an ActiveRecord::Relation + # object, which is composable with other scopes. + # If it returns nil or false, an + # {all}[rdoc-ref:Scoping::Named::ClassMethods#all] scope is returned instead. + # + # A \scope represents a narrowing of a database query, such as # <tt>where(color: :red).select('shirts.*').includes(:washing_instructions)</tt>. # # class Shirt < ActiveRecord::Base @@ -48,12 +53,12 @@ module ActiveRecord # scope :dry_clean_only, -> { joins(:washing_instructions).where('washing_instructions.dry_clean_only = ?', true) } # end # - # The above calls to +scope+ define class methods <tt>Shirt.red</tt> and + # The above calls to #scope define class methods <tt>Shirt.red</tt> and # <tt>Shirt.dry_clean_only</tt>. <tt>Shirt.red</tt>, in effect, # represents the query <tt>Shirt.where(color: 'red')</tt>. # # You should always pass a callable object to the scopes defined - # with +scope+. This ensures that the scope is re-evaluated each + # with #scope. This ensures that the scope is re-evaluated each # time it is called. # # Note that this is simply 'syntactic sugar' for defining an actual @@ -66,14 +71,15 @@ module ActiveRecord # end # # Unlike <tt>Shirt.find(...)</tt>, however, the object returned by - # <tt>Shirt.red</tt> is not an Array; it resembles the association object - # constructed by a +has_many+ declaration. For instance, you can invoke - # <tt>Shirt.red.first</tt>, <tt>Shirt.red.count</tt>, + # <tt>Shirt.red</tt> is not an Array but an ActiveRecord::Relation, + # which is composable with other scopes; it resembles the association object + # constructed by a {has_many}[rdoc-ref:Associations::ClassMethods#has_many] + # declaration. For instance, you can invoke <tt>Shirt.red.first</tt>, <tt>Shirt.red.count</tt>, # <tt>Shirt.red.where(size: 'small')</tt>. Also, just as with the # association objects, named \scopes act like an Array, implementing # Enumerable; <tt>Shirt.red.each(&block)</tt>, <tt>Shirt.red.first</tt>, # and <tt>Shirt.red.inject(memo, &block)</tt> all behave as if - # <tt>Shirt.red</tt> really was an Array. + # <tt>Shirt.red</tt> really was an array. # # These named \scopes are composable. For instance, # <tt>Shirt.red.dry_clean_only</tt> will produce all shirts that are @@ -84,7 +90,8 @@ module ActiveRecord # # All scopes are available as class methods on the ActiveRecord::Base # descendant upon which the \scopes were defined. But they are also - # available to +has_many+ associations. If, + # available to {has_many}[rdoc-ref:Associations::ClassMethods#has_many] + # associations. If, # # class Person < ActiveRecord::Base # has_many :shirts @@ -93,8 +100,8 @@ module ActiveRecord # then <tt>elton.shirts.red.dry_clean_only</tt> will return all of # Elton's red, dry clean only shirts. # - # \Named scopes can also have extensions, just as with +has_many+ - # declarations: + # \Named scopes can also have extensions, just as with + # {has_many}[rdoc-ref:Associations::ClassMethods#has_many] declarations: # # class Shirt < ActiveRecord::Base # scope :red, -> { where(color: 'red') } do diff --git a/activerecord/lib/active_record/secure_token.rb b/activerecord/lib/active_record/secure_token.rb index ca11853da7..8abda2ac49 100644 --- a/activerecord/lib/active_record/secure_token.rb +++ b/activerecord/lib/active_record/secure_token.rb @@ -3,7 +3,7 @@ module ActiveRecord extend ActiveSupport::Concern module ClassMethods - # Example using has_secure_token + # Example using #has_secure_token # # # Schema: User(token:string, auth_token:string) # class User < ActiveRecord::Base @@ -18,11 +18,11 @@ module ActiveRecord # user.regenerate_token # => true # user.regenerate_auth_token # => true # - # SecureRandom::base58 is used to generate the 24-character unique token, so collisions are highly unlikely. + # <tt>SecureRandom::base58</tt> is used to generate the 24-character unique token, so collisions are highly unlikely. # # Note that it's still possible to generate a race condition in the database in the same way that - # <tt>validates_uniqueness_of</tt> can. You're encouraged to add a unique index in the database to deal - # with this even more unlikely scenario. + # {validates_uniqueness_of}[rdoc-ref:Validations::ClassMethods#validates_uniqueness_of] can. + # You're encouraged to add a unique index in the database to deal with this even more unlikely scenario. def has_secure_token(attribute = :token) # Load securerandom only when has_secure_token is used. require 'active_support/core_ext/securerandom' diff --git a/activerecord/lib/active_record/serialization.rb b/activerecord/lib/active_record/serialization.rb index 23dc6465af..5a408e7b8e 100644 --- a/activerecord/lib/active_record/serialization.rb +++ b/activerecord/lib/active_record/serialization.rb @@ -1,5 +1,5 @@ module ActiveRecord #:nodoc: - # = Active Record Serialization + # = Active Record \Serialization module Serialization extend ActiveSupport::Concern include ActiveModel::Serializers::JSON diff --git a/activerecord/lib/active_record/statement_cache.rb b/activerecord/lib/active_record/statement_cache.rb index 95986c820c..f6b0efb88a 100644 --- a/activerecord/lib/active_record/statement_cache.rb +++ b/activerecord/lib/active_record/statement_cache.rb @@ -7,12 +7,14 @@ module ActiveRecord # Book.where(name: "my book").where("author_id > 3") # end # - # The cached statement is executed by using the +execute+ method: + # The cached statement is executed by using the + # [connection.execute]{rdoc-ref:ConnectionAdapters::DatabaseStatements#execute} method: # # cache.execute([], Book, Book.connection) # - # The relation returned by the block is cached, and for each +execute+ call the cached relation gets duped. - # Database is queried when +to_a+ is called on the relation. + # The relation returned by the block is cached, and for each + # [execute]{rdoc-ref:ConnectionAdapters::DatabaseStatements#execute} + # call the cached relation gets duped. Database is queried when +to_a+ is called on the relation. # # If you want to cache the statement without the values you can use the +bind+ method of the # block parameter. diff --git a/activerecord/lib/active_record/store.rb b/activerecord/lib/active_record/store.rb index 919bc58ba5..1b407f7702 100644 --- a/activerecord/lib/active_record/store.rb +++ b/activerecord/lib/active_record/store.rb @@ -16,7 +16,8 @@ module ActiveRecord # JSON, YAML, Marshal are supported out of the box. Generally it can be any wrapper that provides +load+ and +dump+. # # NOTE: If you are using PostgreSQL specific columns like +hstore+ or +json+ there is no need for - # the serialization provided by +store+. Simply use +store_accessor+ instead to generate + # the serialization provided by {.store}[rdoc-ref:rdoc-ref:ClassMethods#store]. + # Simply use {.store_accessor}[rdoc-ref:ClassMethods#store_accessor] instead to generate # the accessor methods. Be aware that these columns use a string keyed hash and do not allow access # using a symbol. # @@ -43,7 +44,7 @@ module ActiveRecord # store_accessor :settings, :privileges, :servants # end # - # The stored attribute names can be retrieved using +stored_attributes+. + # The stored attribute names can be retrieved using {.stored_attributes}[rdoc-ref:rdoc-ref:ClassMethods#stored_attributes]. # # User.stored_attributes[:settings] # [:color, :homepage] # diff --git a/activerecord/lib/active_record/tasks/database_tasks.rb b/activerecord/lib/active_record/tasks/database_tasks.rb index f1141a8613..c0c29a618c 100644 --- a/activerecord/lib/active_record/tasks/database_tasks.rb +++ b/activerecord/lib/active_record/tasks/database_tasks.rb @@ -5,7 +5,7 @@ module ActiveRecord class DatabaseAlreadyExists < StandardError; end # :nodoc: class DatabaseNotSupported < StandardError; end # :nodoc: - # <tt>ActiveRecord::Tasks::DatabaseTasks</tt> is a utility class, which encapsulates + # ActiveRecord::Tasks::DatabaseTasks is a utility class, which encapsulates # logic behind common tasks used to manage database and migrations. # # The tasks defined here are used with Rake tasks provided by Active Record. @@ -18,15 +18,15 @@ module ActiveRecord # # The possible config values are: # - # * +env+: current environment (like Rails.env). - # * +database_configuration+: configuration of your databases (as in +config/database.yml+). - # * +db_dir+: your +db+ directory. - # * +fixtures_path+: a path to fixtures directory. - # * +migrations_paths+: a list of paths to directories with migrations. - # * +seed_loader+: an object which will load seeds, it needs to respond to the +load_seed+ method. - # * +root+: a path to the root of the application. + # * +env+: current environment (like Rails.env). + # * +database_configuration+: configuration of your databases (as in +config/database.yml+). + # * +db_dir+: your +db+ directory. + # * +fixtures_path+: a path to fixtures directory. + # * +migrations_paths+: a list of paths to directories with migrations. + # * +seed_loader+: an object which will load seeds, it needs to respond to the +load_seed+ method. + # * +root+: a path to the root of the application. # - # Example usage of +DatabaseTasks+ outside Rails could look as such: + # Example usage of DatabaseTasks outside Rails could look as such: # # include ActiveRecord::Tasks # DatabaseTasks.database_configuration = YAML.load_file('my_database_config.yml') @@ -94,8 +94,9 @@ module ActiveRecord rescue DatabaseAlreadyExists $stderr.puts "#{configuration['database']} already exists" rescue Exception => error - $stderr.puts error, *(error.backtrace) + $stderr.puts error $stderr.puts "Couldn't create database for #{configuration.inspect}" + raise end def create_all @@ -115,8 +116,9 @@ module ActiveRecord rescue ActiveRecord::NoDatabaseError $stderr.puts "Database '#{configuration['database']}' does not exist" rescue Exception => error - $stderr.puts error, *(error.backtrace) + $stderr.puts error $stderr.puts "Couldn't drop #{configuration['database']}" + raise end def drop_all diff --git a/activerecord/lib/active_record/tasks/mysql_database_tasks.rb b/activerecord/lib/active_record/tasks/mysql_database_tasks.rb index 8929aa85c8..100df9c6c6 100644 --- a/activerecord/lib/active_record/tasks/mysql_database_tasks.rb +++ b/activerecord/lib/active_record/tasks/mysql_database_tasks.rb @@ -1,8 +1,6 @@ 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 @@ -87,12 +85,6 @@ module ActiveRecord Hash.new.tap do |options| options[:charset] = configuration['encoding'] if configuration.include? 'encoding' options[:collation] = configuration['collation'] if configuration.include? 'collation' - - # Set default charset only when collation isn't set. - options[:charset] ||= DEFAULT_CHARSET unless options[:collation] - - # Set default collation only when charset is also default. - options[:collation] ||= DEFAULT_COLLATION if options[:charset] == DEFAULT_CHARSET end end diff --git a/activerecord/lib/active_record/tasks/postgresql_database_tasks.rb b/activerecord/lib/active_record/tasks/postgresql_database_tasks.rb index 55f839444b..cd7d949239 100644 --- a/activerecord/lib/active_record/tasks/postgresql_database_tasks.rb +++ b/activerecord/lib/active_record/tasks/postgresql_database_tasks.rb @@ -54,7 +54,7 @@ module ActiveRecord ActiveRecord::Base.dump_schemas end - args = ['-i', '-s', '-x', '-O', '-f', filename] + args = ['-s', '-x', '-O', '-f', filename] unless search_path.blank? args << search_path.split(',').map do |part| "--schema=#{part.strip}" diff --git a/activerecord/lib/active_record/tasks/sqlite_database_tasks.rb b/activerecord/lib/active_record/tasks/sqlite_database_tasks.rb index 9ab64d0325..9ec3c8a94a 100644 --- a/activerecord/lib/active_record/tasks/sqlite_database_tasks.rb +++ b/activerecord/lib/active_record/tasks/sqlite_database_tasks.rb @@ -19,11 +19,15 @@ module ActiveRecord path = Pathname.new configuration['database'] file = path.absolute? ? path.to_s : File.join(root, path) - FileUtils.rm(file) if File.exist?(file) + FileUtils.rm(file) + rescue Errno::ENOENT => error + raise NoDatabaseError.new(error.message, error) end def purge drop + rescue NoDatabaseError + ensure create end diff --git a/activerecord/lib/active_record/timestamp.rb b/activerecord/lib/active_record/timestamp.rb index e759475cfb..a572c109d8 100644 --- a/activerecord/lib/active_record/timestamp.rb +++ b/activerecord/lib/active_record/timestamp.rb @@ -1,5 +1,5 @@ module ActiveRecord - # = Active Record Timestamp + # = Active Record \Timestamp # # Active Record automatically timestamps create and update operations if the # table has fields named <tt>created_at/created_on</tt> or @@ -17,7 +17,7 @@ module ActiveRecord # # Active Record keeps all the <tt>datetime</tt> and <tt>time</tt> columns # time-zone aware. By default, these values are stored in the database as UTC - # and converted back to the current Time.zone when pulled from the database. + # and converted back to the current <tt>Time.zone</tt> when pulled from the database. # # This feature can be turned off completely by setting: # diff --git a/activerecord/lib/active_record/transactions.rb b/activerecord/lib/active_record/transactions.rb index 1a2988ea77..8de82feae3 100644 --- a/activerecord/lib/active_record/transactions.rb +++ b/activerecord/lib/active_record/transactions.rb @@ -17,10 +17,10 @@ module ActiveRecord # = Active Record Transactions # - # Transactions are protective blocks where SQL statements are only permanent + # \Transactions are protective blocks where SQL statements are only permanent # if they can all succeed as one atomic action. The classic example is a # transfer between two accounts where you can only have a deposit if the - # withdrawal succeeded and vice versa. Transactions enforce the integrity of + # withdrawal succeeded and vice versa. \Transactions enforce the integrity of # the database and guard the data against program errors or database # break-downs. So basically you should use transaction blocks whenever you # have a number of statements that must be executed together or not at all. @@ -40,20 +40,20 @@ module ActiveRecord # # == Different Active Record classes in a single transaction # - # Though the transaction class method is called on some Active Record class, + # Though the #transaction class method is called on some Active Record class, # the objects within the transaction block need not all be instances of # that class. This is because transactions are per-database connection, not # per-model. # # In this example a +balance+ record is transactionally saved even - # though +transaction+ is called on the +Account+ class: + # though #transaction is called on the +Account+ class: # # Account.transaction do # balance.save! # account.save! # end # - # The +transaction+ method is also available as a model instance method. + # The #transaction method is also available as a model instance method. # For example, you can also do this: # # balance.transaction do @@ -80,7 +80,8 @@ module ActiveRecord # # == +save+ and +destroy+ are automatically wrapped in a transaction # - # Both +save+ and +destroy+ come wrapped in a transaction that ensures + # Both {#save}[rdoc-ref:Persistence#save] and + # {#destroy}[rdoc-ref:Persistence#destroy] come wrapped in a transaction that ensures # that whatever you do in validations or callbacks will happen under its # protected cover. So you can use validations to check for values that # the transaction depends on or you can raise exceptions in the callbacks @@ -89,7 +90,7 @@ module ActiveRecord # As a consequence changes to the database are not seen outside your connection # until the operation is complete. For example, if you try to update the index # of a search engine in +after_save+ the indexer won't see the updated record. - # The +after_commit+ callback is the only one that is triggered once the update + # The #after_commit callback is the only one that is triggered once the update # is committed. See below. # # == Exception handling and rolling back @@ -98,11 +99,11 @@ module ActiveRecord # be propagated (after triggering the ROLLBACK), so you should be ready to # catch those in your application code. # - # One exception is the <tt>ActiveRecord::Rollback</tt> exception, which will trigger + # One exception is the ActiveRecord::Rollback exception, which will trigger # a ROLLBACK when raised, but not be re-raised by the transaction block. # - # *Warning*: one should not catch <tt>ActiveRecord::StatementInvalid</tt> exceptions - # inside a transaction block. <tt>ActiveRecord::StatementInvalid</tt> exceptions indicate that an + # *Warning*: one should not catch ActiveRecord::StatementInvalid exceptions + # inside a transaction block. ActiveRecord::StatementInvalid exceptions indicate that an # error occurred at the database level, for example when a unique constraint # is violated. On some database systems, such as PostgreSQL, database errors # inside a transaction cause the entire transaction to become unusable @@ -128,11 +129,11 @@ module ActiveRecord # end # # One should restart the entire transaction if an - # <tt>ActiveRecord::StatementInvalid</tt> occurred. + # ActiveRecord::StatementInvalid occurred. # # == Nested transactions # - # +transaction+ calls can be nested. By default, this makes all database + # #transaction calls can be nested. By default, this makes all database # statements in the nested transaction block become part of the parent # transaction. For example, the following behavior may be surprising: # @@ -144,7 +145,7 @@ module ActiveRecord # end # end # - # creates both "Kotori" and "Nemu". Reason is the <tt>ActiveRecord::Rollback</tt> + # creates both "Kotori" and "Nemu". Reason is the ActiveRecord::Rollback # exception in the nested block does not issue a ROLLBACK. Since these exceptions # are captured in transaction blocks, the parent block does not see it and the # real transaction is committed. @@ -171,19 +172,19 @@ module ActiveRecord # http://dev.mysql.com/doc/refman/5.7/en/savepoint.html # for more information about savepoints. # - # === Callbacks + # === \Callbacks # # There are two types of callbacks associated with committing and rolling back transactions: - # +after_commit+ and +after_rollback+. + # #after_commit and #after_rollback. # - # +after_commit+ callbacks are called on every record saved or destroyed within a - # transaction immediately after the transaction is committed. +after_rollback+ callbacks + # #after_commit callbacks are called on every record saved or destroyed within a + # transaction immediately after the transaction is committed. #after_rollback callbacks # are called on every record saved or destroyed within a transaction immediately after the # transaction or savepoint is rolled back. # # These callbacks are useful for interacting with other systems since you will be guaranteed # that the callback is only executed when the database is in a permanent state. For example, - # +after_commit+ is a good spot to put in a hook to clearing a cache since clearing it from + # #after_commit is a good spot to put in a hook to clearing a cache since clearing it from # within a transaction could trigger the cache to be regenerated before the database is updated. # # === Caveats @@ -234,7 +235,7 @@ module ActiveRecord # This callback is called after a create, update, or destroy are rolled back. # - # Please check the documentation of +after_commit+ for options. + # Please check the documentation of #after_commit for options. def after_rollback(*args, &block) set_options_for_callbacks!(args) set_callback(:rollback, :after, *args, &block) @@ -323,7 +324,7 @@ module ActiveRecord _run_before_commit_callbacks end - # Call the +after_commit+ callbacks. + # Call the #after_commit callbacks. # # Ensure that it is not called if the object was never persisted (failed create), # but call it after the commit of a destroyed object. @@ -336,7 +337,7 @@ module ActiveRecord force_clear_transaction_record_state end - # Call the +after_rollback+ callbacks. The +force_restore_state+ argument indicates if the record + # Call the #after_rollback callbacks. The +force_restore_state+ argument indicates if the record # state should be rolled back to the beginning or just to the last savepoint. def rolledback!(force_restore_state: false, should_run_callbacks: true) #:nodoc: if should_run_callbacks @@ -348,7 +349,7 @@ module ActiveRecord clear_transaction_record_state end - # Add the record to the current transaction so that the +after_rollback+ and +after_commit+ callbacks + # Add the record to the current transaction so that the #after_rollback and #after_commit callbacks # can be called. def add_to_transaction if has_transactional_callbacks? @@ -457,23 +458,23 @@ module ActiveRecord !_rollback_callbacks.empty? || !_commit_callbacks.empty? || !_before_commit_callbacks.empty? end - # Updates the attributes on this particular ActiveRecord object so that - # if it's associated with a transaction, then the state of the ActiveRecord + # Updates the attributes on this particular Active Record object so that + # if it's associated with a transaction, then the state of the Active Record # object will be updated to reflect the current state of the transaction # - # The @transaction_state variable stores the states of the associated + # The +@transaction_state+ variable stores the states of the associated # transaction. This relies on the fact that a transaction can only be in # one rollback or commit (otherwise a list of states would be required) - # Each ActiveRecord object inside of a transaction carries that transaction's + # Each Active Record object inside of a transaction carries that transaction's # TransactionState. # # This method checks to see if the ActiveRecord object's state reflects - # the TransactionState, and rolls back or commits the ActiveRecord object + # the TransactionState, and rolls back or commits the Active Record object # as appropriate. # - # Since ActiveRecord objects can be inside multiple transactions, this + # Since Active Record objects can be inside multiple transactions, this # method recursively goes through the parent of the TransactionState and - # checks if the ActiveRecord object reflects the state of the object. + # checks if the Active Record object reflects the state of the object. def sync_with_transaction_state update_attributes_from_transaction_state(@transaction_state) end diff --git a/activerecord/lib/active_record/type.rb b/activerecord/lib/active_record/type.rb index 74dfe88349..e210e94f00 100644 --- a/activerecord/lib/active_record/type.rb +++ b/activerecord/lib/active_record/type.rb @@ -22,13 +22,13 @@ module ActiveRecord delegate :add_modifier, to: :registry # Add a new type to the registry, allowing it to be referenced as a - # symbol by ActiveRecord::Attributes::ClassMethods#attribute. If your - # type is only meant to be used with a specific database adapter, you can - # do so by passing +adapter: :postgresql+. If your type has the same + # symbol by {ActiveRecord::Base.attribute}[rdoc-ref:Attributes::ClassMethods#attribute]. + # If your type is only meant to be used with a specific database adapter, you can + # do so by passing <tt>adapter: :postgresql</tt>. If your type has the same # name as a native type for the current adapter, an exception will be - # raised unless you specify an +:override+ option. +override: true+ will - # cause your type to be used instead of the native type. +override: - # false+ will cause the native type to be used over yours if one exists. + # raised unless you specify an +:override+ option. <tt>override: true</tt> will + # cause your type to be used instead of the native type. <tt>override: + # false</tt> will cause the native type to be used over yours if one exists. def register(type_name, klass = nil, **options, &block) registry.register(type_name, klass, **options, &block) end diff --git a/activerecord/lib/active_record/type/type_map.rb b/activerecord/lib/active_record/type/type_map.rb index 81d7ed39bb..850a7a4e09 100644 --- a/activerecord/lib/active_record/type/type_map.rb +++ b/activerecord/lib/active_record/type/type_map.rb @@ -1,4 +1,4 @@ -require 'concurrent' +require 'concurrent/map' module ActiveRecord module Type diff --git a/activerecord/lib/active_record/validations.rb b/activerecord/lib/active_record/validations.rb index 4113ca4561..6677e6dc5f 100644 --- a/activerecord/lib/active_record/validations.rb +++ b/activerecord/lib/active_record/validations.rb @@ -1,8 +1,9 @@ module ActiveRecord - # = Active Record RecordInvalid + # = Active Record \RecordInvalid # - # Raised by <tt>save!</tt> and <tt>create!</tt> when the record is invalid. Use the - # +record+ method to retrieve the record which did not validate. + # Raised by {ActiveRecord::Base#save!}[rdoc-ref:Persistence#save!] and + # {ActiveRecord::Base#create!}[rdoc-ref:Persistence::ClassMethods#create!] when the record is invalid. + # Use the #record method to retrieve the record which did not validate. # # begin # complex_operation_that_internally_calls_save! @@ -25,26 +26,26 @@ module ActiveRecord end end - # = Active Record Validations + # = Active Record \Validations # - # Active Record includes the majority of its validations from <tt>ActiveModel::Validations</tt> + # Active Record includes the majority of its validations from ActiveModel::Validations # all of which accept the <tt>:on</tt> argument to define the context where the # validations are active. Active Record will always supply either the context of # <tt>:create</tt> or <tt>:update</tt> dependent on whether the model is a - # <tt>new_record?</tt>. + # {new_record?}[rdoc-ref:Persistence#new_record?]. module Validations extend ActiveSupport::Concern include ActiveModel::Validations # The validation process on save can be skipped by passing <tt>validate: false</tt>. - # The regular Base#save method is replaced with this when the validations - # module is mixed in, which it is by default. + # The regular {ActiveRecord::Base#save}[rdoc-ref:Persistence#save] method is replaced + # with this when the validations module is mixed in, which it is by default. def save(options={}) perform_validations(options) ? super : false end - # Attempts to save the record just like Base#save but will raise a +RecordInvalid+ - # exception instead of returning +false+ if the record is not valid. + # Attempts to save the record just like {ActiveRecord::Base#save}[rdoc-ref:Base#save] but + # will raise a ActiveRecord::RecordInvalid exception instead of returning +false+ if the record is not valid. def save!(options={}) perform_validations(options) ? super : raise_validation_error end @@ -52,12 +53,12 @@ module ActiveRecord # Runs all the validations within the specified context. Returns +true+ if # no errors are found, +false+ otherwise. # - # Aliased as validate. + # Aliased as #validate. # # If the argument is +false+ (default is +nil+), the context is set to <tt>:create</tt> if - # <tt>new_record?</tt> is +true+, and to <tt>:update</tt> if it is not. + # {new_record?}[rdoc-ref:Persistence#new_record?] is +true+, and to <tt>:update</tt> if it is not. # - # Validations with no <tt>:on</tt> option will run no matter the context. Validations with + # \Validations with no <tt>:on</tt> option will run no matter the context. \Validations with # some <tt>:on</tt> option will only run in the specified context. def valid?(context = nil) context ||= default_validation_context diff --git a/activerecord/lib/active_record/validations/associated.rb b/activerecord/lib/active_record/validations/associated.rb index 47ccef31a5..32fbaf0a91 100644 --- a/activerecord/lib/active_record/validations/associated.rb +++ b/activerecord/lib/active_record/validations/associated.rb @@ -24,7 +24,8 @@ module ActiveRecord # # 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+. + # guaranteed to be valid, you also need to use + # {validates_presence_of}[rdoc-ref:Validations::ClassMethods#validates_presence_of]. # # Configuration options: # diff --git a/activerecord/lib/active_record/validations/presence.rb b/activerecord/lib/active_record/validations/presence.rb index 23a3985d35..7e85ed43ac 100644 --- a/activerecord/lib/active_record/validations/presence.rb +++ b/activerecord/lib/active_record/validations/presence.rb @@ -31,7 +31,7 @@ module ActiveRecord # 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 + # This validator defers to the Active Model 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 @@ -39,7 +39,8 @@ module ActiveRecord # # NOTE: This validation will not fail while using it with an association # if the latter was assigned but not valid. If you want to ensure that - # it is both present and valid, you also need to use +validates_associated+. + # it is both present and valid, you also need to use + # {validates_associated}[rdoc-ref:Validations::ClassMethods#validates_associated]. # # Configuration options: # * <tt>:message</tt> - A custom error message (default is: "can't be blank"). @@ -57,7 +58,7 @@ module ActiveRecord # 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. + # See ActiveModel::Validation#validates! for more information. def validates_presence_of(*attr_names) validates_with PresenceValidator, _merge_attributes(attr_names) end diff --git a/activerecord/lib/active_record/validations/uniqueness.rb b/activerecord/lib/active_record/validations/uniqueness.rb index 5706bbd903..aa2794f120 100644 --- a/activerecord/lib/active_record/validations/uniqueness.rb +++ b/activerecord/lib/active_record/validations/uniqueness.rb @@ -172,7 +172,8 @@ module ActiveRecord # # === Concurrency and integrity # - # Using this validation method in conjunction with ActiveRecord::Base#save + # Using this validation method in conjunction with + # {ActiveRecord::Base#save}[rdoc-ref:Persistence#save] # does not guarantee the absence of duplicate record insertions, because # uniqueness checks on the application level are inherently prone to race # conditions. For example, suppose that two users try to post a Comment at @@ -209,12 +210,12 @@ module ActiveRecord # This could even happen if you use transactions with the 'serializable' # isolation level. The best way to work around this problem is to add a unique # index to the database table using - # ActiveRecord::ConnectionAdapters::SchemaStatements#add_index. In the - # rare case that a race condition occurs, the database will guarantee + # {connection.add_index}[rdoc-ref:ConnectionAdapters::SchemaStatements#add_index]. + # In the rare case that a race condition occurs, the database will guarantee # the field's uniqueness. # # When the database catches such a duplicate insertion, - # ActiveRecord::Base#save will raise an ActiveRecord::StatementInvalid + # {ActiveRecord::Base#save}[rdoc-ref:Persistence#save] will raise an ActiveRecord::StatementInvalid # exception. You can either choose to let this error propagate (which # will result in the default Rails exception page being shown), or you # can catch it and restart the transaction (e.g. by telling the user diff --git a/activerecord/lib/rails/generators/active_record/migration.rb b/activerecord/lib/rails/generators/active_record/migration.rb index b7418cf42f..c2b2209638 100644 --- a/activerecord/lib/rails/generators/active_record/migration.rb +++ b/activerecord/lib/rails/generators/active_record/migration.rb @@ -13,6 +13,13 @@ module ActiveRecord ActiveRecord::Migration.next_migration_number(next_migration_number) end end + + private + + def primary_key_type + key_type = options[:primary_key_type] + ", id: :#{key_type}" if key_type + end end end end diff --git a/activerecord/lib/rails/generators/active_record/migration/migration_generator.rb b/activerecord/lib/rails/generators/active_record/migration/migration_generator.rb index 0d57de4d65..4e5872b585 100644 --- a/activerecord/lib/rails/generators/active_record/migration/migration_generator.rb +++ b/activerecord/lib/rails/generators/active_record/migration/migration_generator.rb @@ -5,6 +5,8 @@ module ActiveRecord class MigrationGenerator < Base # :nodoc: argument :attributes, :type => :array, :default => [], :banner => "field[:type][:index] field[:type][:index]" + class_option :primary_key_type, type: :string, desc: "The type for primary key" + def create_migration_file set_local_assigns! validate_file_name! diff --git a/activerecord/lib/rails/generators/active_record/migration/templates/create_table_migration.rb b/activerecord/lib/rails/generators/active_record/migration/templates/create_table_migration.rb index 5b3e57dcf6..fadab2a1e6 100644 --- a/activerecord/lib/rails/generators/active_record/migration/templates/create_table_migration.rb +++ b/activerecord/lib/rails/generators/active_record/migration/templates/create_table_migration.rb @@ -1,6 +1,6 @@ class <%= migration_class_name %> < ActiveRecord::Migration def change - create_table :<%= table_name %> do |t| + create_table :<%= table_name %><%= primary_key_type %> do |t| <% attributes.each do |attribute| -%> <% if attribute.password_digest? -%> t.string :password_digest<%= attribute.inject_options %> diff --git a/activerecord/lib/rails/generators/active_record/model/model_generator.rb b/activerecord/lib/rails/generators/active_record/model/model_generator.rb index 7e8d68ce69..395951ac9d 100644 --- a/activerecord/lib/rails/generators/active_record/model/model_generator.rb +++ b/activerecord/lib/rails/generators/active_record/model/model_generator.rb @@ -7,14 +7,13 @@ module ActiveRecord check_class_collision - class_option :migration, :type => :boolean - class_option :timestamps, :type => :boolean - class_option :parent, :type => :string, :desc => "The parent class for the generated model" - class_option :indexes, :type => :boolean, :default => true, :desc => "Add indexes for references and belongs_to columns" + class_option :migration, type: :boolean + class_option :timestamps, type: :boolean + class_option :parent, type: :string, desc: "The parent class for the generated model" + class_option :indexes, type: :boolean, default: true, desc: "Add indexes for references and belongs_to columns" + class_option :primary_key_type, type: :string, desc: "The type for primary key" - # creates the migration file for the model. - def create_migration_file return unless options[:migration] && options[:parent].nil? attributes.each { |a| a.attr_options.delete(:index) if a.reference? && !a.has_index? } if options[:indexes] == false |