diff options
Diffstat (limited to 'activerecord/lib/active_record')
90 files changed, 1599 insertions, 1249 deletions
diff --git a/activerecord/lib/active_record/aggregations.rb b/activerecord/lib/active_record/aggregations.rb index 3db8e0716b..8101f7a45e 100644 --- a/activerecord/lib/active_record/aggregations.rb +++ b/activerecord/lib/active_record/aggregations.rb @@ -16,8 +16,8 @@ module ActiveRecord # the database). # # class Customer < ActiveRecord::Base - # composed_of :balance, :class_name => "Money", :mapping => %w(balance amount) - # composed_of :address, :mapping => [ %w(address_street street), %w(address_city city) ] + # composed_of :balance, class_name: "Money", mapping: %w(balance amount) + # composed_of :address, mapping: [ %w(address_street street), %w(address_city city) ] # end # # The customer class now has the following methods to manipulate the value objects: @@ -138,15 +138,15 @@ module ActiveRecord # # class NetworkResource < ActiveRecord::Base # composed_of :cidr, - # :class_name => 'NetAddr::CIDR', - # :mapping => [ %w(network_address network), %w(cidr_range bits) ], - # :allow_nil => true, - # :constructor => Proc.new { |network_address, cidr_range| NetAddr::CIDR.create("#{network_address}/#{cidr_range}") }, - # :converter => Proc.new { |value| NetAddr::CIDR.create(value.is_a?(Array) ? value.join('/') : value) } + # class_name: 'NetAddr::CIDR', + # mapping: [ %w(network_address network), %w(cidr_range bits) ], + # allow_nil: true, + # constructor: Proc.new { |network_address, cidr_range| NetAddr::CIDR.create("#{network_address}/#{cidr_range}") }, + # converter: Proc.new { |value| NetAddr::CIDR.create(value.is_a?(Array) ? value.join('/') : value) } # end # # # This calls the :constructor - # network_resource = NetworkResource.new(:network_address => '192.168.0.1', :cidr_range => 24) + # network_resource = NetworkResource.new(network_address: '192.168.0.1', cidr_range: 24) # # # These assignments will both use the :converter # network_resource.cidr = [ '192.168.2.1', 8 ] @@ -165,7 +165,7 @@ module ActiveRecord # by specifying an instance of the value object in the conditions hash. The following example # finds all customers with +balance_amount+ equal to 20 and +balance_currency+ equal to "USD": # - # Customer.where(:balance => Money.new(20, "USD")).all + # Customer.where(balance: Money.new(20, "USD")).all # module ClassMethods # Adds reader and writer methods for manipulating a value object: @@ -197,17 +197,17 @@ module ActiveRecord # can return nil to skip the assignment. # # Option examples: - # composed_of :temperature, :mapping => %w(reading celsius) - # composed_of :balance, :class_name => "Money", :mapping => %w(balance amount), - # :converter => Proc.new { |balance| balance.to_money } - # composed_of :address, :mapping => [ %w(address_street street), %w(address_city city) ] + # composed_of :temperature, mapping: %w(reading celsius) + # composed_of :balance, class_name: "Money", mapping: %w(balance amount), + # converter: Proc.new { |balance| balance.to_money } + # composed_of :address, mapping: [ %w(address_street street), %w(address_city city) ] # composed_of :gps_location - # composed_of :gps_location, :allow_nil => true + # composed_of :gps_location, allow_nil: true # composed_of :ip_address, - # :class_name => 'IPAddr', - # :mapping => %w(ip to_i), - # :constructor => Proc.new { |ip| IPAddr.new(ip, Socket::AF_INET) }, - # :converter => Proc.new { |ip| ip.is_a?(Integer) ? IPAddr.new(ip, Socket::AF_INET) : IPAddr.new(ip.to_s) } + # class_name: 'IPAddr', + # mapping: %w(ip to_i), + # constructor: Proc.new { |ip| IPAddr.new(ip, Socket::AF_INET) }, + # converter: Proc.new { |ip| ip.is_a?(Integer) ? IPAddr.new(ip, Socket::AF_INET) : IPAddr.new(ip.to_s) } # def composed_of(part_id, options = {}) options.assert_valid_keys(:class_name, :mapping, :allow_nil, :constructor, :converter) diff --git a/activerecord/lib/active_record/associations.rb b/activerecord/lib/active_record/associations.rb index 60b7118d7e..651b17920c 100644 --- a/activerecord/lib/active_record/associations.rb +++ b/activerecord/lib/active_record/associations.rb @@ -1,6 +1,9 @@ require 'active_support/core_ext/enumerable' require 'active_support/core_ext/string/conversions' require 'active_support/core_ext/module/remove_method' +require 'active_support/dependencies/autoload' +require 'active_support/concern' +require 'active_record/errors' module ActiveRecord class InverseOfAssociationNotFoundError < ActiveRecordError #:nodoc: @@ -190,10 +193,10 @@ module ActiveRecord # * <tt>Project#portfolio, Project#portfolio=(portfolio), Project#portfolio.nil?</tt> # * <tt>Project#project_manager, Project#project_manager=(project_manager), Project#project_manager.nil?,</tt> # * <tt>Project#milestones.empty?, Project#milestones.size, Project#milestones, Project#milestones<<(milestone),</tt> - # <tt>Project#milestones.delete(milestone), Project#milestones.find(milestone_id), Project#milestones.all(options),</tt> - # <tt>Project#milestones.build, Project#milestones.create</tt> + # <tt>Project#milestones.delete(milestone), Project#milestones.destroy(mileston), Project#milestones.find(milestone_id),</tt> + # <tt>Project#milestones.all(options), Project#milestones.build, Project#milestones.create</tt> # * <tt>Project#categories.empty?, Project#categories.size, Project#categories, Project#categories<<(category1),</tt> - # <tt>Project#categories.delete(category1)</tt> + # <tt>Project#categories.delete(category1), Project#categories.destroy(category1)</tt> # # === A word of warning # @@ -236,6 +239,7 @@ module ActiveRecord # others.clear | X | X | X # others.delete(other,other,...) | X | X | X # others.delete_all | X | X | X + # others.destroy(other,other,...) | X | X | X # others.destroy_all | X | X | X # others.find(*args) | X | X | X # others.exists? | X | X | X @@ -304,11 +308,11 @@ module ActiveRecord # end # class Programmer < ActiveRecord::Base # has_many :assignments - # has_many :projects, :through => :assignments + # has_many :projects, through: :assignments # end # class Project < ActiveRecord::Base # has_many :assignments - # has_many :programmers, :through => :assignments + # has_many :programmers, through: :assignments # end # # For the second way, use +has_and_belongs_to_many+ in both models. This requires a join table @@ -425,7 +429,7 @@ module ActiveRecord # object from an association collection. # # class Project - # has_and_belongs_to_many :developers, :after_add => :evaluate_velocity + # has_and_belongs_to_many :developers, after_add: :evaluate_velocity # # def evaluate_velocity(developer) # ... @@ -436,7 +440,7 @@ module ActiveRecord # # class Project # has_and_belongs_to_many :developers, - # :after_add => [:evaluate_velocity, Proc.new { |p, d| p.shipping_date = Time.now}] + # after_add: [:evaluate_velocity, Proc.new { |p, d| p.shipping_date = Time.now}] # end # # Possible callbacks are: +before_add+, +after_add+, +before_remove+ and +after_remove+. @@ -506,7 +510,7 @@ module ActiveRecord # # class Author < ActiveRecord::Base # has_many :authorships - # has_many :books, :through => :authorships + # has_many :books, through: :authorships # end # # class Authorship < ActiveRecord::Base @@ -522,7 +526,7 @@ module ActiveRecord # # class Firm < ActiveRecord::Base # has_many :clients - # has_many :invoices, :through => :clients + # has_many :invoices, through: :clients # end # # class Client < ActiveRecord::Base @@ -542,7 +546,7 @@ module ActiveRecord # # class Group < ActiveRecord::Base # has_many :users - # has_many :avatars, :through => :users + # has_many :avatars, through: :users # end # # class User < ActiveRecord::Base @@ -570,7 +574,7 @@ module ActiveRecord # works correctly (where <tt>tags</tt> is a +has_many+ <tt>:through</tt> association): # # @post = Post.first - # @tag = @post.tags.build :name => "ruby" + # @tag = @post.tags.build name: "ruby" # @tag.save # # The last line ought to save the through record (a <tt>Taggable</tt>). This will only work if the @@ -578,7 +582,7 @@ module ActiveRecord # # class Taggable < ActiveRecord::Base # belongs_to :post - # belongs_to :tag, :inverse_of => :taggings + # belongs_to :tag, inverse_of: :taggings # end # # == Nested Associations @@ -588,8 +592,8 @@ module ActiveRecord # # class Author < ActiveRecord::Base # has_many :posts - # has_many :comments, :through => :posts - # has_many :commenters, :through => :comments + # has_many :comments, through: :posts + # has_many :commenters, through: :comments # end # # class Post < ActiveRecord::Base @@ -607,12 +611,12 @@ module ActiveRecord # # class Author < ActiveRecord::Base # has_many :posts - # has_many :commenters, :through => :posts + # has_many :commenters, through: :posts # end # # class Post < ActiveRecord::Base # has_many :comments - # has_many :commenters, :through => :comments + # has_many :commenters, through: :comments # end # # class Comment < ActiveRecord::Base @@ -631,11 +635,11 @@ module ActiveRecord # must adhere to. # # class Asset < ActiveRecord::Base - # belongs_to :attachable, :polymorphic => true + # belongs_to :attachable, polymorphic: true # end # # class Post < ActiveRecord::Base - # has_many :assets, :as => :attachable # The :as option specifies the polymorphic interface to use. + # has_many :assets, as: :attachable # The :as option specifies the polymorphic interface to use. # end # # @asset.attachable = @post @@ -652,7 +656,7 @@ module ActiveRecord # column in the posts table. # # class Asset < ActiveRecord::Base - # belongs_to :attachable, :polymorphic => true + # belongs_to :attachable, polymorphic: true # # def attachable_type=(sType) # super(sType.to_s.classify.constantize.base_class.to_s) @@ -660,8 +664,8 @@ module ActiveRecord # end # # class Post < ActiveRecord::Base - # # because we store "Post" in attachable_type now :dependent => :destroy will work - # has_many :assets, :as => :attachable, :dependent => :destroy + # # because we store "Post" in attachable_type now dependent: :destroy will work + # has_many :assets, as: :attachable, dependent: :destroy # end # # class GuestPost < Post @@ -723,7 +727,7 @@ module ActiveRecord # # To include a deep hierarchy of associations, use a hash: # - # Post.includes(:author, {:comments => {:author => :gravatar}}).each do |post| + # Post.includes(:author, {comments: {author: :gravatar}}).each do |post| # # That'll grab not only all the comments but all their authors and gravatar pictures. # You can mix and match symbols, arrays and hashes in any combination to describe the @@ -748,13 +752,13 @@ module ActiveRecord # In the above example posts with no approved comments are not returned at all, because # the conditions apply to the SQL statement as a whole and not just to the association. # You must disambiguate column references for this fallback to happen, for example - # <tt>:order => "author.name DESC"</tt> will work but <tt>:order => "name DESC"</tt> will not. + # <tt>order: "author.name DESC"</tt> will work but <tt>order: "name DESC"</tt> will not. # # If you do want eager load only some members of an association it is usually more natural # 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) @@ -766,7 +770,7 @@ module ActiveRecord # returning all the associated objects: # # class Picture < ActiveRecord::Base - # has_many :most_recent_comments, -> { order('id DESC').limit(10) }, :class_name => 'Comment' + # has_many :most_recent_comments, -> { order('id DESC').limit(10) }, class_name: 'Comment' # end # # Picture.includes(:most_recent_comments).first.most_recent_comments # => returns all associated comments. @@ -774,7 +778,7 @@ module ActiveRecord # Eager loading is supported with polymorphic associations. # # class Address < ActiveRecord::Base - # belongs_to :addressable, :polymorphic => true + # belongs_to :addressable, polymorphic: true # end # # A call that tries to eager load the addressable model @@ -808,10 +812,10 @@ module ActiveRecord # # TreeMixin.joins(:children) # # => SELECT ... FROM mixins INNER JOIN mixins childrens_mixins ... - # TreeMixin.joins(:children => :parent) + # TreeMixin.joins(children: :parent) # # => SELECT ... FROM mixins INNER JOIN mixins childrens_mixins ... # INNER JOIN parents_mixins ... - # TreeMixin.joins(:children => {:parent => :children}) + # TreeMixin.joins(children: {parent: :children}) # # => SELECT ... FROM mixins INNER JOIN mixins childrens_mixins ... # INNER JOIN parents_mixins ... # INNER JOIN mixins childrens_mixins_2 @@ -820,10 +824,10 @@ module ActiveRecord # # Post.joins(:categories) # # => SELECT ... FROM posts INNER JOIN categories_posts ... INNER JOIN categories ... - # Post.joins(:categories => :posts) + # Post.joins(categories: :posts) # # => SELECT ... FROM posts INNER JOIN categories_posts ... INNER JOIN categories ... # INNER JOIN categories_posts posts_categories_join INNER JOIN posts posts_categories - # Post.joins(:categories => {:posts => :categories}) + # Post.joins(categories: {posts: :categories}) # # => SELECT ... FROM posts INNER JOIN categories_posts ... INNER JOIN categories ... # 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 @@ -867,7 +871,7 @@ module ActiveRecord # # module Billing # class Account < ActiveRecord::Base - # belongs_to :firm, :class_name => "MyApplication::Business::Firm" + # belongs_to :firm, class_name: "MyApplication::Business::Firm" # end # end # end @@ -909,16 +913,16 @@ module ActiveRecord # example, if we changed our model definitions to: # # class Dungeon < ActiveRecord::Base - # has_many :traps, :inverse_of => :dungeon - # has_one :evil_wizard, :inverse_of => :dungeon + # has_many :traps, inverse_of: :dungeon + # has_one :evil_wizard, inverse_of: :dungeon # end # # class Trap < ActiveRecord::Base - # belongs_to :dungeon, :inverse_of => :traps + # belongs_to :dungeon, inverse_of: :traps # end # # class EvilWizard < ActiveRecord::Base - # belongs_to :dungeon, :inverse_of => :evil_wizard + # belongs_to :dungeon, inverse_of: :evil_wizard # end # # Then, from our code snippet above, +d+ and <tt>t.dungeon</tt> are actually the same @@ -941,7 +945,7 @@ module ActiveRecord # For example: # # class Author - # has_many :posts, :dependent => :destroy + # has_many :posts, dependent: :destroy # end # Author.find(1).destroy # => Will destroy all of the author's posts, too # @@ -950,6 +954,11 @@ module ActiveRecord # specific association types. When no option is given, the behaviour is to do nothing # with the associated records when destroying a record. # + # Note that <tt>:dependent</tt> is implemented using Rails' callback + # system, which works by processing callbacks in order. Therefore, other + # callbacks declared either before or after the <tt>:dependent</tt> option + # can affect what it does. + # # === Delete or destroy? # # +has_many+ and +has_and_belongs_to_many+ associations have the methods <tt>destroy</tt>, @@ -1020,12 +1029,18 @@ module ActiveRecord # parent object. # [collection.delete(object, ...)] # Removes one or more objects from the collection by setting their foreign keys to +NULL+. - # Objects will be in addition destroyed if they're associated with <tt>:dependent => :destroy</tt>, - # and deleted if they're associated with <tt>:dependent => :delete_all</tt>. + # Objects will be in addition destroyed if they're associated with <tt>dependent: :destroy</tt>, + # and deleted if they're associated with <tt>dependent: :delete_all</tt>. # # If the <tt>:through</tt> option is used, then the join records are deleted (rather than - # nullified) by default, but you can specify <tt>:dependent => :destroy</tt> or - # <tt>:dependent => :nullify</tt> to override this. + # nullified) by default, but you can specify <tt>dependent: :destroy</tt> or + # <tt>dependent: :nullify</tt> to override this. + # [collection.destroy(object, ...)] + # Removes one or more objects from the collection by running <tt>destroy</tt> on + # each record, regardless of any dependent option, ensuring callbacks are run. + # + # If the <tt>:through</tt> option is used, then the join records are destroyed + # instead, not the objects themselves. # [collection=objects] # Replaces the collections content by deleting and adding objects as appropriate. If the <tt>:through</tt> # option is true callbacks in the join models are triggered except destroy callbacks, since deletion is @@ -1037,8 +1052,8 @@ module ActiveRecord # method loads the models and calls <tt>collection=</tt>. See above. # [collection.clear] # Removes every object from the collection. This destroys the associated objects if they - # are associated with <tt>:dependent => :destroy</tt>, deletes them directly from the - # database if <tt>:dependent => :delete_all</tt>, otherwise sets their foreign keys to +NULL+. + # are associated with <tt>dependent: :destroy</tt>, deletes them directly from the + # database if <tt>dependent: :delete_all</tt>, otherwise sets their foreign keys to +NULL+. # If the <tt>:through</tt> option is true no destroy callbacks are invoked on the join models. # Join models are directly deleted. # [collection.empty?] @@ -1066,17 +1081,18 @@ module ActiveRecord # === Example # # Example: A Firm class declares <tt>has_many :clients</tt>, which will add: - # * <tt>Firm#clients</tt> (similar to <tt>Clients.all :conditions => ["firm_id = ?", id]</tt>) + # * <tt>Firm#clients</tt> (similar to <tt>Clients.all conditions: ["firm_id = ?", id]</tt>) # * <tt>Firm#clients<<</tt> # * <tt>Firm#clients.delete</tt> + # * <tt>Firm#clients.destroy</tt> # * <tt>Firm#clients=</tt> # * <tt>Firm#client_ids</tt> # * <tt>Firm#client_ids=</tt> # * <tt>Firm#clients.clear</tt> # * <tt>Firm#clients.empty?</tt> (similar to <tt>firm.clients.size == 0</tt>) # * <tt>Firm#clients.size</tt> (similar to <tt>Client.count "firm_id = #{id}"</tt>) - # * <tt>Firm#clients.find</tt> (similar to <tt>Client.find(id, :conditions => "firm_id = #{id}")</tt>) - # * <tt>Firm#clients.exists?(:name => 'ACME')</tt> (similar to <tt>Client.exists?(:name => 'ACME', :firm_id => firm.id)</tt>) + # * <tt>Firm#clients.find</tt> (similar to <tt>Client.find(id, conditions: "firm_id = #{id}")</tt>) + # * <tt>Firm#clients.exists?(name: 'ACME')</tt> (similar to <tt>Client.exists?(name: 'ACME', firm_id: firm.id)</tt>) # * <tt>Firm#clients.build</tt> (similar to <tt>Client.new("firm_id" => id)</tt>) # * <tt>Firm#clients.create</tt> (similar to <tt>c = Client.new("firm_id" => id); c.save; c</tt>) # The declaration can also include an options hash to specialize the behavior of the association. @@ -1095,7 +1111,10 @@ module ActiveRecord # Specify the method that returns the primary key used for the association. By default this is +id+. # [:dependent] # Controls what happens to the associated objects when - # their owner is destroyed: + # their owner is destroyed. Note that these are implemented as + # callbacks, and Rails executes callbacks in order. Therefore, other + # similar callbacks may affect the :dependent behavior, and the + # :dependent behavior may affect other callbacks. # # * <tt>:destroy</tt> causes all the associated objects to also be destroyed # * <tt>:delete_all</tt> causes all the asssociated objects to be deleted directly from the database (so callbacks will not execute) @@ -1106,6 +1125,9 @@ module ActiveRecord # If using with the <tt>:through</tt> option, the association on the join model must be # a +belongs_to+, and the records which get deleted are the join records, rather than # the associated records. + # [: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. # [:as] # Specifies a polymorphic interface (See <tt>belongs_to</tt>). # [:through] @@ -1127,7 +1149,7 @@ module ActiveRecord # [:source] # Specifies the source association name used by <tt>has_many :through</tt> queries. # Only use it if the name cannot be inferred from the association. - # <tt>has_many :subscribers, :through => :subscriptions</tt> will look for either <tt>:subscribers</tt> or + # <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 @@ -1191,7 +1213,7 @@ module ActiveRecord # === Example # # An Account class declares <tt>has_one :beneficiary</tt>, which will add: - # * <tt>Account#beneficiary</tt> (similar to <tt>Beneficiary.first(:conditions => "account_id = #{id}")</tt>) + # * <tt>Account#beneficiary</tt> (similar to <tt>Beneficiary.first(conditions: "account_id = #{id}")</tt>) # * <tt>Account#beneficiary=(beneficiary)</tt> (similar to <tt>beneficiary.account_id = account.id; beneficiary.save</tt>) # * <tt>Account#build_beneficiary</tt> (similar to <tt>Beneficiary.new("account_id" => id)</tt>) # * <tt>Account#create_beneficiary</tt> (similar to <tt>b = Beneficiary.new("account_id" => id); b.save; b</tt>) @@ -1231,7 +1253,7 @@ module ActiveRecord # [:source] # Specifies the source association name used by <tt>has_one :through</tt> queries. # Only use it if the name cannot be inferred from the association. - # <tt>has_one :favorite, :through => :favorites</tt> will look for a + # <tt>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 @@ -1251,11 +1273,11 @@ module ActiveRecord # See ActiveRecord::Associations::ClassMethods's overview on Bi-directional associations for more detail. # # Option examples: - # has_one :credit_card, :dependent => :destroy # destroys the associated credit card - # has_one :credit_card, :dependent => :nullify # updates the associated records foreign + # has_one :credit_card, dependent: :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: :true # has_one :club, through: :membership @@ -1310,12 +1332,12 @@ module ActiveRecord # Specify the foreign key used for the association. By default this is guessed to be the name # of the association with an "_id" suffix. So a class that defines a <tt>belongs_to :person</tt> # association will use "person_id" as the default <tt>:foreign_key</tt>. Similarly, - # <tt>belongs_to :favorite_person, :class_name => "Person"</tt> will use a foreign key + # <tt>belongs_to :favorite_person, class_name: "Person"</tt> will use a foreign key # of "favorite_person_id". # [:foreign_type] # Specify the column used to store the associated object's type, if this is a polymorphic # association. By default this is guessed to be the name of the association with a "_type" - # suffix. So a class that defines a <tt>belongs_to :taggable, :polymorphic => true</tt> + # suffix. So a class that defines a <tt>belongs_to :taggable, polymorphic: true</tt> # association will use "taggable_type" as the default <tt>:foreign_type</tt>. # [:primary_key] # Specify the method that returns the primary key of associated object used for the association. @@ -1335,7 +1357,7 @@ module ActiveRecord # <tt>#{table_name}_count</tt> is created on the associate class (such that Post.comments_count will # return the count cached, see note below). You can also specify a custom counter # cache column by providing a column name instead of a +true+/+false+ value to this - # option (e.g., <tt>:counter_cache => :my_custom_counter</tt>.) + # option (e.g., <tt>counter_cache: :my_custom_counter</tt>.) # Note: Specifying a counter cache will add it to that model's list of readonly attributes # using +attr_readonly+. # [:polymorphic] @@ -1393,7 +1415,7 @@ module ActiveRecord # # class CreateDevelopersProjectsJoinTable < ActiveRecord::Migration # def change - # create_table :developers_projects, :id => false do |t| + # create_table :developers_projects, id: false do |t| # t.integer :developer_id # t.integer :project_id # end @@ -1417,6 +1439,9 @@ module ActiveRecord # [collection.delete(object, ...)] # Removes one or more objects from the collection by removing their associations from the join table. # This does not destroy the objects. + # [collection.destroy(object, ...)] + # Removes one or more objects from the collection by running destroy on each association in the join table, overriding any dependent option. + # This does not destroy the objects. # [collection=objects] # Replaces the collection's content by deleting and adding objects as appropriate. # [collection_singular_ids] @@ -1453,6 +1478,7 @@ module ActiveRecord # * <tt>Developer#projects</tt> # * <tt>Developer#projects<<</tt> # * <tt>Developer#projects.delete</tt> + # * <tt>Developer#projects.destroy</tt> # * <tt>Developer#projects=</tt> # * <tt>Developer#project_ids</tt> # * <tt>Developer#project_ids=</tt> diff --git a/activerecord/lib/active_record/associations/alias_tracker.rb b/activerecord/lib/active_record/associations/alias_tracker.rb index 84540a7000..0c23029981 100644 --- a/activerecord/lib/active_record/associations/alias_tracker.rb +++ b/activerecord/lib/active_record/associations/alias_tracker.rb @@ -8,7 +8,7 @@ module ActiveRecord attr_reader :aliases, :table_joins, :connection # table_joins is an array of arel joins which might conflict with the aliases we assign here - def initialize(connection = ActiveRecord::Model.connection, table_joins = []) + def initialize(connection = Base.connection, table_joins = []) @aliases = Hash.new { |h,k| h[k] = initial_count_for(k) } @table_joins = table_joins @connection = connection diff --git a/activerecord/lib/active_record/associations/association.rb b/activerecord/lib/active_record/associations/association.rb index 495f0cde59..99e7383d42 100644 --- a/activerecord/lib/active_record/associations/association.rb +++ b/activerecord/lib/active_record/associations/association.rb @@ -85,7 +85,7 @@ module ActiveRecord end def scoped - ActiveSupport::Deprecation.warn("#scoped is deprecated. use #scope instead.") + ActiveSupport::Deprecation.warn "#scoped is deprecated. use #scope instead." scope end @@ -154,11 +154,8 @@ module ActiveRecord # We can't dump @reflection since it contains the scope proc def marshal_dump - reflection = @reflection - @reflection = nil - - ivars = instance_variables.map { |name| [name, instance_variable_get(name)] } - [reflection.name, ivars] + ivars = (instance_variables - [:@reflection]).map { |name| [name, instance_variable_get(name)] } + [@reflection.name, ivars] end def marshal_load(data) @@ -226,7 +223,7 @@ module ActiveRecord end # This should be implemented to return the values of the relevant key(s) on the owner, - # so that when state_state is different from the value stored on the last find_target, + # so that when stale_state is different from the value stored on the last find_target, # the target is stale. # # This is only relevant to certain associations, which is why it returns nil by default. diff --git a/activerecord/lib/active_record/associations/builder/collection_association.rb b/activerecord/lib/active_record/associations/builder/collection_association.rb index 1b382f7285..fcdfc1e150 100644 --- a/activerecord/lib/active_record/associations/builder/collection_association.rb +++ b/activerecord/lib/active_record/associations/builder/collection_association.rb @@ -1,5 +1,8 @@ +require 'active_record/associations' + module ActiveRecord::Associations::Builder class CollectionAssociation < Association #:nodoc: + CALLBACKS = [:before_add, :after_add, :before_remove, :after_remove] def valid_options diff --git a/activerecord/lib/active_record/associations/builder/has_many.rb b/activerecord/lib/active_record/associations/builder/has_many.rb index ab8225460a..0d1bdd21ee 100644 --- a/activerecord/lib/active_record/associations/builder/has_many.rb +++ b/activerecord/lib/active_record/associations/builder/has_many.rb @@ -5,7 +5,7 @@ module ActiveRecord::Associations::Builder end def valid_options - super + [:primary_key, :dependent, :as, :through, :source, :source_type, :inverse_of] + super + [:primary_key, :dependent, :as, :through, :source, :source_type, :inverse_of, :counter_cache] end def valid_dependent_options diff --git a/activerecord/lib/active_record/associations/collection_association.rb b/activerecord/lib/active_record/associations/collection_association.rb index fe3e5b00f7..862ff201de 100644 --- a/activerecord/lib/active_record/associations/collection_association.rb +++ b/activerecord/lib/active_record/associations/collection_association.rb @@ -364,6 +364,16 @@ module ActiveRecord record end + def scope(opts = {}) + scope = super() + scope.none! if opts.fetch(:nullify, true) && null_scope? + scope + end + + def null_scope? + owner.new_record? && !foreign_key_present? + end + private def custom_counter_sql @@ -412,7 +422,7 @@ module ActiveRecord persisted.map! do |record| if mem_record = memory.delete(record) - (record.attribute_names - mem_record.changes.keys).each do |name| + ((record.attribute_names & mem_record.attribute_names) - mem_record.changes.keys).each do |name| mem_record[name] = record[name] end @@ -574,7 +584,9 @@ module ActiveRecord args.shift if args.first.is_a?(Hash) && args.first.empty? collection = fetch_first_or_last_using_find?(args) ? scope : load_target - collection.send(type, *args).tap {|it| set_inverse_instance it } + collection.send(type, *args).tap do |record| + set_inverse_instance record if record.is_a? ActiveRecord::Base + end end end end diff --git a/activerecord/lib/active_record/associations/collection_proxy.rb b/activerecord/lib/active_record/associations/collection_proxy.rb index c113957faa..e444b0ed83 100644 --- a/activerecord/lib/active_record/associations/collection_proxy.rb +++ b/activerecord/lib/active_record/associations/collection_proxy.rb @@ -28,10 +28,12 @@ module ActiveRecord # is computed directly through SQL and does not trigger by itself the # instantiation of the actual post records. class CollectionProxy < Relation + delegate(*(ActiveRecord::Calculations.public_instance_methods - [:count]), to: :scope) + def initialize(association) #:nodoc: @association = association super association.klass, association.klass.arel_table - merge! association.scope + merge! association.scope(nullify: false) end def target @@ -42,11 +44,15 @@ module ActiveRecord @association.load_target end + # Returns +true+ if the association has been loaded, otherwise +false+. + # + # person.pets.loaded? # => false + # person.pets + # person.pets.loaded? # => true def loaded? @association.loaded? end - ## # Works in two ways. # # *First:* Specify a subset of fields to be selected from the result set. @@ -104,9 +110,8 @@ module ActiveRecord @association.select(select, &block) end - ## # Finds an object in the collection responding to the +id+. Uses the same - # rules as +ActiveRecord::Base.find+. Returns +ActiveRecord::RecordNotFound++ + # rules as <tt>ActiveRecord::Base.find</tt>. Returns <tt>ActiveRecord::RecordNotFound</tt> # error if the object can not be found. # # class Person < ActiveRecord::Base @@ -135,7 +140,6 @@ module ActiveRecord @association.find(*args, &block) end - ## # Returns the first record, or the first +n+ records, from the collection. # If the collection is empty, the first form returns +nil+, and the second # form returns an empty array. @@ -166,7 +170,6 @@ module ActiveRecord @association.first(*args) end - ## # Returns the last record, or the last +n+ records, from the collection. # If the collection is empty, the first form returns +nil+, and the second # form returns an empty array. @@ -197,7 +200,6 @@ module ActiveRecord @association.last(*args) end - ## # Returns a new object of the collection type that has been instantiated # with +attributes+ and linked to this object, but have not yet been saved. # You can pass an array of attributes hashes, this will return an array @@ -226,7 +228,6 @@ module ActiveRecord @association.build(attributes, &block) end - ## # Returns a new object of the collection type that has been instantiated with # attributes, linked to this object and that has already been saved (if it # passes the validations). @@ -257,7 +258,6 @@ module ActiveRecord @association.create(attributes, &block) end - ## # Like +create+, except that if the record is invalid, raises an exception. # # class Person @@ -274,7 +274,6 @@ module ActiveRecord @association.create!(attributes, &block) end - ## # Add one or more records to the collection by setting their foreign keys # to the association's primary key. Since << flattens its argument list and # inserts each record, +push+ and +concat+ behave identically. Returns +self+ @@ -303,7 +302,6 @@ module ActiveRecord @association.concat(*records) end - ## # Replace this collection with +other_array+. This will perform a diff # and delete/add only records that have changed. # @@ -330,7 +328,6 @@ module ActiveRecord @association.replace(other_array) end - ## # Deletes all the records from the collection. For +has_many+ associations, # the deletion is done according to the strategy specified by the <tt>:dependent</tt> # option. Returns an array with the deleted records. @@ -423,7 +420,6 @@ module ActiveRecord @association.delete_all end - ## # Deletes the records of the collection directly from the database. # This will _always_ remove the records ignoring the +:dependent+ # option. @@ -450,7 +446,6 @@ module ActiveRecord @association.destroy_all end - ## # Deletes the +records+ supplied and removes them from the collection. For # +has_many+ associations, the deletion is done according to the strategy # specified by the <tt>:dependent</tt> option. Returns an array with the @@ -514,7 +509,7 @@ module ActiveRecord # Pet.find(1, 3) # # => ActiveRecord::RecordNotFound: Couldn't find all Pets with IDs (1, 3) # - # If it is set to <tt>:delete_all</tt>, all the +records+ are deleted + # If it is set to <tt>:delete_all</tt>, all the +records+ are deleted # *without* calling their +destroy+ method. # # class Person < ActiveRecord::Base @@ -569,7 +564,6 @@ module ActiveRecord @association.delete(*records) end - ## # Destroys the +records+ supplied and removes them from the collection. # This method will _always_ remove record from the database ignoring # the +:dependent+ option. Returns an array with the removed records. @@ -642,7 +636,6 @@ module ActiveRecord @association.destroy(*records) end - ## # Specifies whether the records should be unique or not. # # class Person < ActiveRecord::Base @@ -661,7 +654,6 @@ module ActiveRecord @association.uniq end - ## # Count all records using SQL. # # class Person < ActiveRecord::Base @@ -679,7 +671,6 @@ module ActiveRecord @association.count(column_name, options) end - ## # Returns the size of the collection. If the collection hasn't been loaded, # it executes a <tt>SELECT COUNT(*)</tt> query. # @@ -704,7 +695,6 @@ module ActiveRecord @association.size end - ## # Returns the size of the collection calling +size+ on the target. # If the collection has been already loaded, +length+ and +size+ are # equivalent. @@ -728,7 +718,6 @@ module ActiveRecord @association.length end - ## # Returns +true+ if the collection is empty. # # class Person < ActiveRecord::Base @@ -746,7 +735,6 @@ module ActiveRecord @association.empty? end - ## # Returns +true+ if the collection is not empty. # # class Person < ActiveRecord::Base @@ -780,7 +768,6 @@ module ActiveRecord @association.any?(&block) end - ## # Returns true if the collection has more than one record. # Equivalent to <tt>collection.size > 1</tt>. # @@ -819,7 +806,6 @@ module ActiveRecord @association.many?(&block) end - ## # Returns +true+ if the given object is present in the collection. # # class Person < ActiveRecord::Base @@ -889,7 +875,7 @@ module ActiveRecord end # Returns a new array of objects from the collection. If the collection - # hasn't been loaded, it fetches the records from the database. + # hasn't been loaded, it fetches the records from the database. # # class Person < ActiveRecord::Base # has_many :pets diff --git a/activerecord/lib/active_record/associations/has_many_association.rb b/activerecord/lib/active_record/associations/has_many_association.rb index 74864d271f..f59565ae77 100644 --- a/activerecord/lib/active_record/associations/has_many_association.rb +++ b/activerecord/lib/active_record/associations/has_many_association.rb @@ -76,7 +76,7 @@ module ActiveRecord end def cached_counter_attribute_name(reflection = reflection) - "#{reflection.name}_count" + options[:counter_cache] || "#{reflection.name}_count" end def update_counter(difference, reflection = reflection) diff --git a/activerecord/lib/active_record/associations/has_one_association.rb b/activerecord/lib/active_record/associations/has_one_association.rb index 06bead41de..ee816d2392 100644 --- a/activerecord/lib/active_record/associations/has_one_association.rb +++ b/activerecord/lib/active_record/associations/has_one_association.rb @@ -28,7 +28,7 @@ module ActiveRecord # If target and record are nil, or target is equal to record, # we don't need to have transaction. if (target || record) && target != record - reflection.klass.transaction do + transaction_if(save) do remove_target!(options[:dependent]) if target && !target.destroyed? if record @@ -90,6 +90,14 @@ module ActiveRecord def nullify_owner_attributes(record) record[reflection.foreign_key] = nil end + + def transaction_if(value) + if value + reflection.klass.transaction { yield } + else + yield + end + end end end end diff --git a/activerecord/lib/active_record/associations/preloader.rb b/activerecord/lib/active_record/associations/preloader.rb index ce5bf15f10..0848e7afb3 100644 --- a/activerecord/lib/active_record/associations/preloader.rb +++ b/activerecord/lib/active_record/associations/preloader.rb @@ -30,17 +30,21 @@ module ActiveRecord # option references an association's column), it will fallback to the table # join strategy. class Preloader #:nodoc: - autoload :Association, 'active_record/associations/preloader/association' - autoload :SingularAssociation, 'active_record/associations/preloader/singular_association' - autoload :CollectionAssociation, 'active_record/associations/preloader/collection_association' - autoload :ThroughAssociation, 'active_record/associations/preloader/through_association' + extend ActiveSupport::Autoload - autoload :HasMany, 'active_record/associations/preloader/has_many' - autoload :HasManyThrough, 'active_record/associations/preloader/has_many_through' - autoload :HasOne, 'active_record/associations/preloader/has_one' - autoload :HasOneThrough, 'active_record/associations/preloader/has_one_through' - autoload :HasAndBelongsToMany, 'active_record/associations/preloader/has_and_belongs_to_many' - autoload :BelongsTo, 'active_record/associations/preloader/belongs_to' + eager_autoload do + autoload :Association, 'active_record/associations/preloader/association' + autoload :SingularAssociation, 'active_record/associations/preloader/singular_association' + autoload :CollectionAssociation, 'active_record/associations/preloader/collection_association' + autoload :ThroughAssociation, 'active_record/associations/preloader/through_association' + + autoload :HasMany, 'active_record/associations/preloader/has_many' + autoload :HasManyThrough, 'active_record/associations/preloader/has_many_through' + autoload :HasOne, 'active_record/associations/preloader/has_one' + autoload :HasOneThrough, 'active_record/associations/preloader/has_one_through' + autoload :HasAndBelongsToMany, 'active_record/associations/preloader/has_and_belongs_to_many' + autoload :BelongsTo, 'active_record/associations/preloader/belongs_to' + end attr_reader :records, :associations, :preload_scope, :model @@ -68,7 +72,7 @@ module ActiveRecord # books. # - a Hash which specifies multiple association names, as well as # association names for the to-be-preloaded association objects. For - # example, specifying <tt>{ :author => :avatar }</tt> will preload a + # example, specifying <tt>{ author: :avatar }</tt> will preload a # book's author, as well as that author's avatar. # # +:associations+ has the same format as the +:include+ option for @@ -76,8 +80,8 @@ module ActiveRecord # # :books # [ :books, :author ] - # { :author => :avatar } - # [ :books, { :author => :avatar } ] + # { author: :avatar } + # [ :books, { author: :avatar } ] def initialize(records, associations, preload_scope = nil) @records = Array.wrap(records).compact.uniq @associations = Array.wrap(associations) diff --git a/activerecord/lib/active_record/associations/singular_association.rb b/activerecord/lib/active_record/associations/singular_association.rb index 32f4557c28..10238555f0 100644 --- a/activerecord/lib/active_record/associations/singular_association.rb +++ b/activerecord/lib/active_record/associations/singular_association.rb @@ -12,7 +12,7 @@ module ActiveRecord target end - # Implements the writer method, e.g. foo.items= for Foo.has_many :items + # Implements the writer method, e.g. foo.bar= for Foo.belongs_to :bar def writer(record) replace(record) end diff --git a/activerecord/lib/active_record/associations/through_association.rb b/activerecord/lib/active_record/associations/through_association.rb index b9e014735b..43520142bf 100644 --- a/activerecord/lib/active_record/associations/through_association.rb +++ b/activerecord/lib/active_record/associations/through_association.rb @@ -28,7 +28,7 @@ module ActiveRecord # methods which create and delete records on the association. # # We only support indirectly modifying through associations which has a belongs_to source. - # This is the "has_many :tags, :through => :taggings" situation, where the join model + # This is the "has_many :tags, through: :taggings" situation, where the join model # typically has a belongs_to on both side. In other words, associations which could also # be represented as has_and_belongs_to_many associations. # diff --git a/activerecord/lib/active_record/attribute_assignment.rb b/activerecord/lib/active_record/attribute_assignment.rb index af13b75a9d..6c5e2ac05d 100644 --- a/activerecord/lib/active_record/attribute_assignment.rb +++ b/activerecord/lib/active_record/attribute_assignment.rb @@ -57,9 +57,8 @@ module ActiveRecord # by calling new on the column type or aggregation type (through composed_of) object with these parameters. # So having the pairs written_on(1) = "2004", written_on(2) = "6", written_on(3) = "24", will instantiate # written_on (a date type) with Date.new("2004", "6", "24"). You can also specify a typecast character in the - # parentheses to have the parameters typecasted before they're used in the constructor. Use i for Fixnum, - # f for Float, s for String, and a for Array. If all the values for a given attribute are empty, the - # attribute will be set to +nil+. + # parentheses to have the parameters typecasted before they're used in the constructor. Use i for Fixnum and + # f for Float. If all the values for a given attribute are empty, the attribute will be set to +nil+. def assign_multiparameter_attributes(pairs) execute_callstack_for_multiparameter_attributes( extract_callstack_for_multiparameter_attributes(pairs) diff --git a/activerecord/lib/active_record/attribute_methods.rb b/activerecord/lib/active_record/attribute_methods.rb index ced15bc330..e0bfdb8f3e 100644 --- a/activerecord/lib/active_record/attribute_methods.rb +++ b/activerecord/lib/active_record/attribute_methods.rb @@ -2,7 +2,7 @@ require 'active_support/core_ext/enumerable' module ActiveRecord # = Active Record Attribute Methods - module AttributeMethods #:nodoc: + module AttributeMethods extend ActiveSupport::Concern include ActiveModel::AttributeMethods @@ -20,7 +20,7 @@ module ActiveRecord module ClassMethods # Generates all the attribute related methods for columns in the database # accessors, mutators and query methods. - def define_attribute_methods + def define_attribute_methods # :nodoc: # Use a mutex; we don't want two thread simaltaneously trying to define # attribute methods. @attribute_methods_mutex.synchronize do @@ -31,21 +31,35 @@ module ActiveRecord end end - def attribute_methods_generated? + def attribute_methods_generated? # :nodoc: @attribute_methods_generated ||= false end - def undefine_attribute_methods + def undefine_attribute_methods # :nodoc: super if attribute_methods_generated? @attribute_methods_generated = false end + # Raises a <tt>ActiveRecord::DangerousAttributeError</tt> exception when an + # \Active \Record method is defined in the model, otherwise +false+. + # + # class Person < ActiveRecord::Base + # def save + # 'already defined by Active Record' + # end + # end + # + # Person.instance_method_already_implemented?(:save) + # # => ActiveRecord::DangerousAttributeError: save is defined by ActiveRecord + # + # Person.instance_method_already_implemented?(:name) + # # => false def instance_method_already_implemented?(method_name) if dangerous_attribute_method?(method_name) raise DangerousAttributeError, "#{method_name} is defined by ActiveRecord" end - if [Base, Model].include?(active_record_super) + if superclass == Base super else # If B < A and A defines its own attribute method, then we don't want to overwrite that. @@ -56,11 +70,11 @@ module ActiveRecord # A method name is 'dangerous' if it is already defined by Active Record, but # not by any ancestors. (So 'puts' is not dangerous but 'save' is.) - def dangerous_attribute_method?(name) + def dangerous_attribute_method?(name) # :nodoc: method_defined_within?(name, Base) end - def method_defined_within?(name, klass, sup = klass.superclass) + def method_defined_within?(name, klass, sup = klass.superclass) # :nodoc: if klass.method_defined?(name) || klass.private_method_defined?(name) if sup.method_defined?(name) || sup.private_method_defined?(name) klass.instance_method(name).owner != sup.instance_method(name).owner @@ -72,13 +86,27 @@ module ActiveRecord end end + # Returns +true+ if +attribute+ is an attribute method and table exists, + # +false+ otherwise. + # + # class Person < ActiveRecord::Base + # end + # + # Person.attribute_method?('name') # => true + # Person.attribute_method?(:age=) # => true + # Person.attribute_method?(:nothing) # => false def attribute_method?(attribute) super || (table_exists? && column_names.include?(attribute.to_s.sub(/=$/, ''))) end - # Returns an array of column names as strings if it's not - # an abstract class and table exists. - # Otherwise it returns an empty array. + # Returns an array of column names as strings if it's not an abstract class and + # table exists. Otherwise it returns an empty array. + # + # class Person < ActiveRecord::Base + # end + # + # Person.attribute_names + # # => ["id", "created_at", "updated_at", "name", "age"] def attribute_names @attribute_names ||= if !abstract_class? && table_exists? column_names @@ -90,7 +118,7 @@ module ActiveRecord # If we haven't generated any methods yet, generate them, then # see if we've created the method we're looking for. - def method_missing(method, *args, &block) + def method_missing(method, *args, &block) # :nodoc: unless self.class.attribute_methods_generated? self.class.define_attribute_methods @@ -104,7 +132,7 @@ module ActiveRecord end end - def attribute_missing(match, *args, &block) + def attribute_missing(match, *args, &block) # :nodoc: if self.class.columns_hash[match.attr_name] ActiveSupport::Deprecation.warn( "The method `#{match.method_name}', matching the attribute `#{match.attr_name}' has " \ @@ -118,22 +146,60 @@ module ActiveRecord super end + # A Person object with a name attribute can ask <tt>person.respond_to?(:name)</tt>, + # <tt>person.respond_to?(:name=)</tt>, and <tt>person.respond_to?(:name?)</tt> + # which will all return +true+. It also define the attribute methods if they have + # not been generated. + # + # class Person < ActiveRecord::Base + # end + # + # person = Person.new + # person.respond_to(:name) # => true + # person.respond_to(:name=) # => true + # person.respond_to(:name?) # => true + # person.respond_to('age') # => true + # person.respond_to('age=') # => true + # person.respond_to('age?') # => true + # person.respond_to(:nothing) # => false def respond_to?(name, include_private = false) self.class.define_attribute_methods unless self.class.attribute_methods_generated? super end - # Returns true if the given attribute is in the attributes hash + # Returns +true+ if the given attribute is in the attributes hash, otherwise +false+. + # + # class Person < ActiveRecord::Base + # end + # + # person = Person.new + # person.has_attribute?(:name) # => true + # person.has_attribute?('age') # => true + # person.has_attribute?(:nothing) # => false def has_attribute?(attr_name) @attributes.has_key?(attr_name.to_s) end # Returns an array of names for the attributes available on this object. + # + # class Person < ActiveRecord::Base + # end + # + # person = Person.new + # person.attribute_names + # # => ["id", "created_at", "updated_at", "name", "age"] def attribute_names @attributes.keys end # Returns a hash of all the attributes with their names as keys and the values of the attributes as values. + # + # class Person < ActiveRecord::Base + # end + # + # person = Person.create(name: 'Francesco', age: 22) + # person.attributes + # # => {"id"=>3, "created_at"=>Sun, 21 Oct 2012 04:53:04, "updated_at"=>Sun, 21 Oct 2012 04:53:04, "name"=>"Francesco", "age"=>22} def attributes attribute_names.each_with_object({}) { |name, attrs| attrs[name] = read_attribute(name) @@ -146,13 +212,13 @@ module ActiveRecord # <tt>:db</tt> format. Other attributes return the value of # <tt>#inspect</tt> without modification. # - # person = Person.create!(:name => "David Heinemeier Hansson " * 3) + # person = Person.create!(name: 'David Heinemeier Hansson ' * 3) # # person.attribute_for_inspect(:name) - # # => '"David Heinemeier Hansson David Heinemeier Hansson D..."' + # # => "\"David Heinemeier Hansson David Heinemeier Hansson D...\"" # # person.attribute_for_inspect(:created_at) - # # => '"2009-01-12 04:48:57"' + # # => "\"2012-10-22 00:15:07\"" def attribute_for_inspect(attr_name) value = read_attribute(attr_name) @@ -165,57 +231,103 @@ module ActiveRecord end end - # Returns true if the specified +attribute+ has been set by the user or by a database load and is neither - # nil nor empty? (the latter only applies to objects that respond to empty?, most notably Strings). + # Returns +true+ if the specified +attribute+ has been set by the user or by a + # database load and is neither +nil+ nor <tt>empty?</tt> (the latter only applies + # to objects that respond to <tt>empty?</tt>, most notably Strings). Otherwise, +false+. + # Note that it always returns +true+ with boolean attributes. + # + # class Task < ActiveRecord::Base + # end + # + # person = Task.new(title: '', is_done: false) + # person.attribute_present?(:title) # => false + # person.attribute_present?(:is_done) # => true + # person.name = 'Francesco' + # person.is_done = true + # person.attribute_present?(:title) # => true + # person.attribute_present?(:is_done) # => true def attribute_present?(attribute) value = read_attribute(attribute) !value.nil? && !(value.respond_to?(:empty?) && value.empty?) end - # Returns the column object for the named attribute. + # Returns the column object for the named attribute. Returns +nil+ if the + # named attribute not exists. + # + # class Person < ActiveRecord::Base + # end + # + # person = Person.new + # person.column_for_attribute(:name) # the result depends on the ConnectionAdapter + # # => #<ActiveRecord::ConnectionAdapters::SQLite3Column:0x007ff4ab083980 @name="name", @sql_type="varchar(255)", @null=true, ...> + # + # person.column_for_attribute(:nothing) + # # => nil def column_for_attribute(name) # FIXME: should this return a null object for columns that don't exist? self.class.columns_hash[name.to_s] end # Returns the value of the attribute identified by <tt>attr_name</tt> after it has been typecast (for example, - # "2004-12-12" in a data column is cast to a date object, like Date.new(2004, 12, 12)). - # (Alias for the protected read_attribute method). + # "2004-12-12" in a data column is cast to a date object, like Date.new(2004, 12, 12)). It raises + # <tt>ActiveModel::MissingAttributeError</tt> if the identified attribute is missing. + # + # Alias for the <tt>read_attribute</tt> method. + # + # class Person < ActiveRecord::Base + # belongs_to :organization + # end + # + # person = Person.new(name: 'Francesco', age: '22') + # person[:name] # => "Francesco" + # person[:age] # => 22 + # + # person = Person.select('id').first + # person[:name] # => ActiveModel::MissingAttributeError: missing attribute: name + # person[:organization_id] # => ActiveModel::MissingAttributeError: missing attribute: organization_id def [](attr_name) - read_attribute(attr_name) + read_attribute(attr_name) { |n| missing_attribute(n, caller) } end # Updates the attribute identified by <tt>attr_name</tt> with the specified +value+. - # (Alias for the protected write_attribute method). + # (Alias for the protected <tt>write_attribute</tt> method). + # + # class Person < ActiveRecord::Base + # end + # + # person = Person.new + # person[:age] = '22' + # person[:age] # => 22 + # person[:age] # => Fixnum def []=(attr_name, value) write_attribute(attr_name, value) end protected - def clone_attributes(reader_method = :read_attribute, attributes = {}) + def clone_attributes(reader_method = :read_attribute, attributes = {}) # :nodoc: attribute_names.each do |name| attributes[name] = clone_attribute_value(reader_method, name) end attributes end - def clone_attribute_value(reader_method, attribute_name) + def clone_attribute_value(reader_method, attribute_name) # :nodoc: value = send(reader_method, attribute_name) value.duplicable? ? value.clone : value rescue TypeError, NoMethodError value end - def arel_attributes_with_values_for_create(pk_attribute_allowed) - arel_attributes_with_values(attributes_for_create(pk_attribute_allowed)) + def arel_attributes_with_values_for_create(attribute_names) # :nodoc: + arel_attributes_with_values(attributes_for_create(attribute_names)) end - def arel_attributes_with_values_for_update(attribute_names) + def arel_attributes_with_values_for_update(attribute_names) # :nodoc: arel_attributes_with_values(attributes_for_update(attribute_names)) end - def attribute_method?(attr_name) + def attribute_method?(attr_name) # :nodoc: defined?(@attributes) && @attributes.include?(attr_name) end @@ -242,9 +354,9 @@ module ActiveRecord # Filters out the primary keys, from the attribute names, when the primary # key is to be generated (e.g. the id attribute has no value). - def attributes_for_create(pk_attribute_allowed) - @attributes.keys.select do |name| - column_for_attribute(name) && (pk_attribute_allowed || !pk_attribute?(name)) + def attributes_for_create(attribute_names) + attribute_names.select do |name| + column_for_attribute(name) && !(pk_attribute?(name) && id.nil?) end end @@ -257,14 +369,10 @@ module ActiveRecord end def typecasted_attribute_value(name) - if self.class.serialized_attributes.include?(name) - @attributes[name].serialized_value - else - # FIXME: we need @attributes to be used consistently. - # If the values stored in @attributes were already typecasted, this code - # could be simplified - read_attribute(name) - end + # FIXME: we need @attributes to be used consistently. + # If the values stored in @attributes were already typecasted, this code + # could be simplified + read_attribute(name) end end end 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 d4f529acbf..a23baeaced 100644 --- a/activerecord/lib/active_record/attribute_methods/before_type_cast.rb +++ b/activerecord/lib/active_record/attribute_methods/before_type_cast.rb @@ -1,5 +1,28 @@ module ActiveRecord module AttributeMethods + # = Active Record Attribute Methods Before Type Cast + # + # <tt>ActiveRecord::AttributeMethods::BeforeTypeCast</tt> provides a way to + # read the value of the attributes before typecasting and deserialization. + # + # class Task < ActiveRecord::Base + # end + # + # task = Task.new(id: '1', completed_on: '2012-10-21') + # task.id # => 1 + # task.completed_on # => Sun, 21 Oct 2012 + # + # task.attributes_before_type_cast + # # => {"id"=>"1", "completed_on"=>"2012-10-21", ... } + # task.read_attribute_before_type_cast('id') # => "1" + # task.read_attribute_before_type_cast('completed_on') # => "2012-10-21" + # + # In addition to #read_attribute_before_type_cast and #attributes_before_type_cast, + # it declares a method for all attributes with the <tt>*_before_type_cast</tt> + # suffix. + # + # task.id_before_type_cast # => "1" + # task.completed_on_before_type_cast # => "2012-10-21" module BeforeTypeCast extend ActiveSupport::Concern @@ -7,11 +30,31 @@ module ActiveRecord attribute_method_suffix "_before_type_cast" end + # Returns the value of the attribute identified by +attr_name+ before + # typecasting and deserialization. + # + # class Task < ActiveRecord::Base + # end + # + # task = Task.new(id: '1', completed_on: '2012-10-21') + # task.read_attribute('id') # => 1 + # task.read_attribute_before_type_cast('id') # => '1' + # task.read_attribute('completed_on') # => Sun, 21 Oct 2012 + # task.read_attribute_before_type_cast('completed_on') # => "2012-10-21" def read_attribute_before_type_cast(attr_name) @attributes[attr_name] end # Returns a hash of attributes before typecasting and deserialization. + # + # class Task < ActiveRecord::Base + # end + # + # task = Task.new(title: nil, is_done: true, completed_on: '2012-10-21') + # task.attributes + # # => {"id"=>nil, "title"=>nil, "is_done"=>true, "completed_on"=>Sun, 21 Oct 2012, "created_at"=>nil, "updated_at"=>nil} + # task.attributes_before_type_cast + # # => {"id"=>nil, "title"=>nil, "is_done"=>true, "completed_on"=>"2012-10-21", "created_at"=>nil, "updated_at"=>nil} def attributes_before_type_cast @attributes end diff --git a/activerecord/lib/active_record/attribute_methods/dirty.rb b/activerecord/lib/active_record/attribute_methods/dirty.rb index 60e5b0e2bb..0333605eac 100644 --- a/activerecord/lib/active_record/attribute_methods/dirty.rb +++ b/activerecord/lib/active_record/attribute_methods/dirty.rb @@ -1,13 +1,9 @@ require 'active_support/core_ext/module/attribute_accessors' +require 'active_support/deprecation' module ActiveRecord - ActiveSupport.on_load(:active_record_config) do - mattr_accessor :partial_updates, instance_accessor: false - self.partial_updates = true - end - module AttributeMethods - module Dirty + module Dirty # :nodoc: extend ActiveSupport::Concern include ActiveModel::Dirty @@ -17,11 +13,23 @@ module ActiveRecord raise "You cannot include Dirty after Timestamp" end - config_attribute :partial_updates + class_attribute :partial_writes, instance_writer: false + self.partial_writes = true + + def self.partial_updates=(v); self.partial_writes = v; end + def self.partial_updates?; partial_writes?; end + def self.partial_updates; partial_writes; end + + ActiveSupport::Deprecation.deprecate_methods( + singleton_class, + :partial_updates= => :partial_writes=, + :partial_updates? => :partial_writes?, + :partial_updates => :partial_writes + ) end # Attempts to +save+ the record and clears changed attributes if successful. - def save(*) #:nodoc: + def save(*) if status = super @previously_changed = changes @changed_attributes.clear @@ -30,7 +38,7 @@ module ActiveRecord end # Attempts to <tt>save!</tt> the record and clears changed attributes if successful. - def save!(*) #:nodoc: + def save!(*) super.tap do @previously_changed = changes @changed_attributes.clear @@ -38,7 +46,7 @@ module ActiveRecord end # <tt>reload</tt> the record and clears changed attributes. - def reload(*) #:nodoc: + def reload(*) super.tap do @previously_changed.clear @changed_attributes.clear @@ -64,13 +72,17 @@ module ActiveRecord end def update(*) - if partial_updates? - # Serialized attributes should always be written in case they've been - # changed in place. - super(changed | (attributes.keys & self.class.serialized_attributes.keys)) - else - super - end + partial_writes? ? super(keys_for_partial_write) : super + end + + def create(*) + partial_writes? ? super(keys_for_partial_write) : super + end + + # Serialized attributes should always be written in case they've been + # changed in place. + def keys_for_partial_write + changed | (attributes.keys & self.class.serialized_attributes.keys) end def _field_changed?(attr, old, value) diff --git a/activerecord/lib/active_record/attribute_methods/primary_key.rb b/activerecord/lib/active_record/attribute_methods/primary_key.rb index aa6704d5c9..0857b02c8e 100644 --- a/activerecord/lib/active_record/attribute_methods/primary_key.rb +++ b/activerecord/lib/active_record/attribute_methods/primary_key.rb @@ -5,28 +5,29 @@ module ActiveRecord module PrimaryKey extend ActiveSupport::Concern - # Returns this record's primary key value wrapped in an Array if one is available + # Returns this record's primary key value wrapped in an Array if one is + # available. def to_key key = self.id [key] if key end - # Returns the primary key value + # Returns the primary key value. def id read_attribute(self.class.primary_key) end - # Sets the primary key value + # Sets the primary key value. def id=(value) write_attribute(self.class.primary_key, value) if self.class.primary_key end - # Queries the primary key value + # Queries the primary key value. def id? query_attribute(self.class.primary_key) end - # Returns the primary key value before type cast + # Returns the primary key value before type cast. def id_before_type_cast read_attribute_before_type_cast(self.class.primary_key) end @@ -52,14 +53,16 @@ module ActiveRecord super && !ID_ATTRIBUTE_METHODS.include?(method_name) end - # Defines the primary key field -- can be overridden in subclasses. Overwriting will negate any effect of the - # primary_key_prefix_type setting, though. + # Defines the primary key field -- can be overridden in subclasses. + # Overwriting will negate any effect of the +primary_key_prefix_type+ + # setting, though. def primary_key @primary_key = reset_primary_key unless defined? @primary_key @primary_key end - # Returns a quoted version of the primary key name, used to construct SQL statements. + # Returns a quoted version of the primary key name, used to construct + # SQL statements. def quoted_primary_key @quoted_primary_key ||= connection.quote_column_name(primary_key) end @@ -92,16 +95,17 @@ module ActiveRecord # Sets the name of the primary key column. # # class Project < ActiveRecord::Base - # self.primary_key = "sysid" + # 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 - # "foo_" + super + # 'foo_' + super # end # end + # # Project.primary_key # => "foo_id" def primary_key=(value) @primary_key = value && value.to_s diff --git a/activerecord/lib/active_record/attribute_methods/read.rb b/activerecord/lib/active_record/attribute_methods/read.rb index 1a4cb25dd7..90701938e5 100644 --- a/activerecord/lib/active_record/attribute_methods/read.rb +++ b/activerecord/lib/active_record/attribute_methods/read.rb @@ -1,8 +1,4 @@ module ActiveRecord - ActiveSupport.on_load(:active_record_config) do - mattr_accessor :attribute_types_cached_by_default, instance_accessor: false - end - module AttributeMethods module Read extend ActiveSupport::Concern @@ -10,13 +6,15 @@ module ActiveRecord ATTRIBUTE_TYPES_CACHED_BY_DEFAULT = [:datetime, :timestamp, :time, :date] included do - config_attribute :attribute_types_cached_by_default + class_attribute :attribute_types_cached_by_default, instance_writer: false + self.attribute_types_cached_by_default = ATTRIBUTE_TYPES_CACHED_BY_DEFAULT end module ClassMethods - # +cache_attributes+ allows you to declare which converted attribute values should - # be cached. Usually caching only pays off for attributes with expensive conversion - # methods, like time related columns (e.g. +created_at+, +updated_at+). + # +cache_attributes+ allows you to declare which converted attribute + # values should be cached. Usually caching only pays off for attributes + # with expensive conversion methods, like time related columns (e.g. + # +created_at+, +updated_at+). def cache_attributes(*attribute_names) cached_attributes.merge attribute_names.map { |attr| attr.to_s } end @@ -45,7 +43,7 @@ module ActiveRecord def define_method_attribute(attr_name) generated_attribute_methods.module_eval <<-STR, __FILE__, __LINE__ + 1 def __temp__ - read_attribute(:'#{attr_name}') { |n| missing_attribute(n, caller) } + read_attribute('#{attr_name}') { |n| missing_attribute(n, caller) } end alias_method '#{attr_name}', :__temp__ undef_method :__temp__ @@ -63,22 +61,17 @@ module ActiveRecord end end - ActiveRecord::Model.attribute_types_cached_by_default = ATTRIBUTE_TYPES_CACHED_BY_DEFAULT - - # Returns the value of the attribute identified by <tt>attr_name</tt> after it has been typecast (for example, - # "2004-12-12" in a data column is cast to a date object, like Date.new(2004, 12, 12)). + # Returns the value of the attribute identified by <tt>attr_name</tt> after + # it has been typecast (for example, "2004-12-12" in a data column is cast + # to a date object, like Date.new(2004, 12, 12)). def read_attribute(attr_name) - return unless attr_name - name_sym = attr_name.to_sym - # If it's cached, just return it # We use #[] first as a perf optimization for non-nil values. See https://gist.github.com/3552829. - @attributes_cache[name_sym] || @attributes_cache.fetch(name_sym) { - name = attr_name.to_s - + name = attr_name.to_s + @attributes_cache[name] || @attributes_cache.fetch(name) { column = @columns_hash.fetch(name) { return @attributes.fetch(name) { - if name_sym == :id && self.class.primary_key != name + if name == 'id' && self.class.primary_key != name read_attribute(self.class.primary_key) end } @@ -89,7 +82,7 @@ module ActiveRecord } if self.class.cache_attribute?(name) - @attributes_cache[name_sym] = column.type_cast(value) + @attributes_cache[name] = column.type_cast(value) else column.type_cast value end diff --git a/activerecord/lib/active_record/attribute_methods/serialization.rb b/activerecord/lib/active_record/attribute_methods/serialization.rb index bdda5bc009..47d4a938af 100644 --- a/activerecord/lib/active_record/attribute_methods/serialization.rb +++ b/activerecord/lib/active_record/attribute_methods/serialization.rb @@ -4,17 +4,19 @@ module ActiveRecord extend ActiveSupport::Concern included do - # Returns a hash of all the attributes that have been specified for serialization as - # keys and their class restriction as values. + # Returns a hash of all the attributes that have been specified for + # serialization as keys and their class restriction as values. class_attribute :serialized_attributes, instance_accessor: false self.serialized_attributes = {} end module ClassMethods - # If you have an attribute that needs to be saved to the database as an object, and retrieved as the same object, - # then specify the name of that attribute using this method and it will be handled automatically. - # The serialization is done through YAML. If +class_name+ is specified, the serialized object must be of that - # class on retrieval or SerializationTypeMismatch will be raised. + # If you have an attribute that needs to be saved to the database as an + # object, and retrieved as the same object, then specify the name of that + # attribute using this method and it will be handled automatically. The + # serialization is done through YAML. If +class_name+ is specified, the + # serialized object must be of that class on retrieval or + # <tt>SerializationTypeMismatch</tt> will be raised. # # ==== Parameters # @@ -22,7 +24,8 @@ module ActiveRecord # * +class_name+ - Optional, class name that the object type should be equal to. # # ==== Example - # # Serialize a preferences attribute + # + # # Serialize a preferences attribute. # class User < ActiveRecord::Base # serialize :preferences # end @@ -42,7 +45,8 @@ module ActiveRecord end def serialized_attributes - ActiveSupport::Deprecation.warn("Instance level serialized_attributes method is deprecated, please use class level method.") + message = "Instance level serialized_attributes method is deprecated, please use class level method." + ActiveSupport::Deprecation.warn message defined?(@serialized_attributes) ? @serialized_attributes : self.class.serialized_attributes end @@ -60,7 +64,7 @@ module ActiveRecord end end - class Attribute < Struct.new(:coder, :value, :state) + class Attribute < Struct.new(:coder, :value, :state) # :nodoc: def unserialized_value state == :serialized ? unserialize : value end @@ -98,16 +102,6 @@ module ActiveRecord attributes end - - private - - def attribute_cast_code(attr_name) - if serialized_attributes.include?(attr_name) - "v.unserialized_value" - else - super - end - end end def type_cast_attribute_for_write(column, value) @@ -125,6 +119,24 @@ module ActiveRecord super end end + + def attributes_before_type_cast + super.dup.tap do |attributes| + self.class.serialized_attributes.each_key do |key| + if attributes.key?(key) + attributes[key] = attributes[key].unserialized_value + end + end + end + end + + def typecasted_attribute_value(name) + if self.class.serialized_attributes.include?(name) + @attributes[name].serialized_value + else + super + end + end end end end diff --git a/activerecord/lib/active_record/attribute_methods/time_zone_conversion.rb b/activerecord/lib/active_record/attribute_methods/time_zone_conversion.rb index 9647d03be4..427c61079a 100644 --- a/activerecord/lib/active_record/attribute_methods/time_zone_conversion.rb +++ b/activerecord/lib/active_record/attribute_methods/time_zone_conversion.rb @@ -1,13 +1,4 @@ - module ActiveRecord - ActiveSupport.on_load(:active_record_config) do - mattr_accessor :time_zone_aware_attributes, instance_accessor: false - self.time_zone_aware_attributes = false - - mattr_accessor :skip_time_zone_conversion_for_attributes, instance_accessor: false - self.skip_time_zone_conversion_for_attributes = [] - end - module AttributeMethods module TimeZoneConversion class Type # :nodoc: @@ -28,27 +19,15 @@ module ActiveRecord extend ActiveSupport::Concern included do - config_attribute :time_zone_aware_attributes, global: true - config_attribute :skip_time_zone_conversion_for_attributes + mattr_accessor :time_zone_aware_attributes, instance_writer: false + self.time_zone_aware_attributes = false + + class_attribute :skip_time_zone_conversion_for_attributes, instance_writer: false + self.skip_time_zone_conversion_for_attributes = [] end module ClassMethods protected - # The enhanced read method automatically converts the UTC time stored in the database to the time - # zone stored in Time.zone. - def attribute_cast_code(attr_name) - column = columns_hash[attr_name] - - if create_time_zone_conversion_attribute?(attr_name, column) - typecast = "v = #{super}" - time_zone_conversion = "v.acts_like?(:time) ? v.in_time_zone : v" - - "((#{typecast}) && (#{time_zone_conversion}))" - else - super - end - end - # Defined for all +datetime+ and +timestamp+ attributes when +time_zone_aware_attributes+ are enabled. # This enhanced write method will automatically convert the time passed to it to the zone stored in Time.zone. def define_method_attribute=(attr_name) @@ -65,7 +44,7 @@ module ActiveRecord if (rounded_value != rounded_time) || (!rounded_value && original_time) write_attribute("#{attr_name}", original_time) #{attr_name}_will_change! - @attributes_cache[:"#{attr_name}"] = zoned_time + @attributes_cache["#{attr_name}"] = zoned_time end end EOV @@ -85,8 +64,7 @@ module ActiveRecord private def round_usec(value) - return unless value - value.change(:usec => 0) + value.change(usec: 0) if value end end end diff --git a/activerecord/lib/active_record/attribute_methods/write.rb b/activerecord/lib/active_record/attribute_methods/write.rb index 5a39cb0125..fa9097db1f 100644 --- a/activerecord/lib/active_record/attribute_methods/write.rb +++ b/activerecord/lib/active_record/attribute_methods/write.rb @@ -20,18 +20,19 @@ module ActiveRecord end end - # Updates the attribute identified by <tt>attr_name</tt> with the specified +value+. Empty strings - # for fixnum and float columns are turned into +nil+. + # Updates the attribute identified by <tt>attr_name</tt> with the + # specified +value+. Empty strings for fixnum and float columns are + # turned into +nil+. def write_attribute(attr_name, value) attr_name = attr_name.to_s attr_name = self.class.primary_key if attr_name == 'id' && self.class.primary_key - @attributes_cache.delete(attr_name.to_sym) + @attributes_cache.delete(attr_name) column = column_for_attribute(attr_name) # If we're dealing with a binary column, write the data to the cache # so we don't attempt to typecast multiple times. if column && column.binary? - @attributes_cache[attr_name.to_sym] = value + @attributes_cache[attr_name] = value end if column || @attributes.has_key?(attr_name) diff --git a/activerecord/lib/active_record/autosave_association.rb b/activerecord/lib/active_record/autosave_association.rb index 290f57659d..907fe70522 100644 --- a/activerecord/lib/active_record/autosave_association.rb +++ b/activerecord/lib/active_record/autosave_association.rb @@ -16,7 +16,7 @@ module ActiveRecord # Note that it also means that associations marked for destruction won't # be destroyed directly. They will however still be marked for destruction. # - # Note that <tt>:autosave => false</tt> is not same as not declaring <tt>:autosave</tt>. + # Note that <tt>autosave: false</tt> is not same as not declaring <tt>:autosave</tt>. # When the <tt>:autosave</tt> option is not present new associations are saved. # # == Validation @@ -37,7 +37,7 @@ module ActiveRecord # === One-to-one Example # # class Post - # has_one :author, :autosave => true + # has_one :author, autosave: true # end # # Saving changes to the parent and its associated model can now be performed @@ -81,27 +81,27 @@ module ActiveRecord # has_many :comments # :autosave option is not declared # end # - # post = Post.new(:title => 'ruby rocks') - # post.comments.build(:body => 'hello world') + # post = Post.new(title: 'ruby rocks') + # post.comments.build(body: 'hello world') # post.save # => saves both post and comment # - # post = Post.create(:title => 'ruby rocks') - # post.comments.build(:body => 'hello world') + # post = Post.create(title: 'ruby rocks') + # post.comments.build(body: 'hello world') # post.save # => saves both post and comment # - # post = Post.create(:title => 'ruby rocks') - # post.comments.create(:body => 'hello world') + # post = Post.create(title: 'ruby rocks') + # post.comments.create(body: 'hello world') # post.save # => saves both post and comment # # When <tt>:autosave</tt> is true all children are saved, no matter whether they # are new records or not: # # class Post - # has_many :comments, :autosave => true + # has_many :comments, autosave: true # end # - # post = Post.create(:title => 'ruby rocks') - # post.comments.create(:body => 'hello world') + # post = Post.create(title: 'ruby rocks') + # post.comments.create(body: 'hello world') # post.comments[0].body = 'hi everyone' # post.save # => saves both post and comment, with 'hi everyone' as body # @@ -394,6 +394,7 @@ module ActiveRecord autosave = reflection.options[:autosave] if autosave && record.marked_for_destruction? + self[reflection.foreign_key] = nil record.destroy elsif autosave != false saved = record.save(:validate => !autosave) if record.new_record? || (autosave && record.changed_for_autosave?) diff --git a/activerecord/lib/active_record/base.rb b/activerecord/lib/active_record/base.rb index a4705b24ca..5eacb8f143 100644 --- a/activerecord/lib/active_record/base.rb +++ b/activerecord/lib/active_record/base.rb @@ -8,7 +8,6 @@ require 'active_support/core_ext/class/attribute_accessors' require 'active_support/core_ext/class/delegating_attributes' require 'active_support/core_ext/array/extract_options' require 'active_support/core_ext/hash/deep_merge' -require 'active_support/core_ext/hash/indifferent_access' require 'active_support/core_ext/hash/slice' require 'active_support/core_ext/string/behavior' require 'active_support/core_ext/kernel/singleton_class' @@ -36,7 +35,7 @@ module ActiveRecord #:nodoc: # method is especially useful when you're receiving the data from somewhere else, like an # HTTP request. It works like this: # - # user = User.new(:name => "David", :occupation => "Code Artist") + # user = User.new(name: "David", occupation: "Code Artist") # user.name # => "David" # # You can also use block initialization: @@ -69,7 +68,7 @@ module ActiveRecord #:nodoc: # end # # def self.authenticate_safely_simply(user_name, password) - # where(:user_name => user_name, :password => password).first + # where(user_name: user_name, password: password).first # end # end # @@ -87,27 +86,27 @@ module ActiveRecord #:nodoc: # # Company.where( # "id = :id AND name = :name AND division = :division AND created_at > :accounting_date", - # { :id => 3, :name => "37signals", :division => "First", :accounting_date => '2005-01-01' } + # { id: 3, name: "37signals", division: "First", accounting_date: '2005-01-01' } # ).first # # Similarly, a simple hash without a statement will generate conditions based on equality with the SQL AND # operator. For instance: # - # Student.where(:first_name => "Harvey", :status => 1) + # Student.where(first_name: "Harvey", status: 1) # Student.where(params[:student]) # # A range may be used in the hash to use the SQL BETWEEN operator: # - # Student.where(:grade => 9..12) + # Student.where(grade: 9..12) # # An array may be used in the hash to use the SQL IN operator: # - # Student.where(:grade => [9,11,12]) + # Student.where(grade: [9,11,12]) # # When joining tables, nested hashes or keys written in the form 'table_name.column_name' # can be used to qualify the table name of a particular condition. For instance: # - # Student.joins(:schools).where(:schools => { :category => 'public' }) + # Student.joins(:schools).where(schools: { category: 'public' }) # Student.joins(:schools).where('schools.category' => 'public' ) # # == Overwriting default accessors @@ -141,10 +140,10 @@ module ActiveRecord #:nodoc: # For example, an Active Record User with the <tt>name</tt> attribute has a <tt>name?</tt> method that you can call # to determine whether the user has a name: # - # user = User.new(:name => "David") + # user = User.new(name: "David") # user.name? # => true # - # anonymous = User.new(:name => "") + # anonymous = User.new(name: "") # anonymous.name? # => false # # == Accessing attributes before they have been typecasted @@ -165,8 +164,8 @@ module ActiveRecord #:nodoc: # to <tt>find_by_</tt>, <tt>find_last_by_</tt>, or <tt>find_all_by_</tt> and thus produces finders # like <tt>Person.find_by_user_name</tt>, <tt>Person.find_all_by_last_name</tt>, and # <tt>Payment.find_by_transaction_id</tt>. Instead of writing - # <tt>Person.where(:user_name => user_name).first</tt>, you just do <tt>Person.find_by_user_name(user_name)</tt>. - # And instead of writing <tt>Person.where(:last_name => last_name).all</tt>, you just do + # <tt>Person.where(user_name: user_name).first</tt>, you just do <tt>Person.find_by_user_name(user_name)</tt>. + # And instead of writing <tt>Person.where(last_name: last_name).all</tt>, you just do # <tt>Person.find_all_by_last_name(last_name)</tt>. # # It's possible to add an exclamation point (!) on the end of the dynamic finders to get them to raise an @@ -175,7 +174,7 @@ module ActiveRecord #:nodoc: # # It's also possible to use multiple attributes in the same find by separating them with "_and_". # - # Person.where(:user_name => user_name, :password => password).first + # Person.where(user_name: user_name, password: password).first # Person.find_by_user_name_and_password(user_name, password) # with dynamic finder # # It's even possible to call these dynamic finder methods on relations and named scopes. @@ -189,13 +188,13 @@ module ActiveRecord #:nodoc: # unless they are given in a block. # # # No 'Summer' tag exists - # Tag.find_or_create_by_name("Summer") # equal to Tag.create(:name => "Summer") + # Tag.find_or_create_by_name("Summer") # equal to Tag.create(name: "Summer") # # # Now the 'Summer' tag does exist # Tag.find_or_create_by_name("Summer") # equal to Tag.find_by_name("Summer") # # # Now 'Bob' exist and is an 'admin' - # User.find_or_create_by_name('Bob', :age => 40) { |u| u.admin = true } + # User.find_or_create_by_name('Bob', age: 40) { |u| u.admin = true } # # Adding an exclamation point (!) on to the end of <tt>find_or_create_by_</tt> will # raise an <tt>ActiveRecord::RecordInvalid</tt> error if the new record is invalid. @@ -210,7 +209,7 @@ module ActiveRecord #:nodoc: # To find by a subset of the attributes to be used for instantiating a new object, pass a hash instead of # a list of parameters. # - # Tag.find_or_create_by_name(:name => "rails", :creator => current_user) + # Tag.find_or_create_by_name(name: "rails", creator: current_user) # # That will either find an existing tag named "rails", or create a new one while setting the # user that created it. @@ -232,7 +231,7 @@ module ActiveRecord #:nodoc: # serialize :preferences # end # - # user = User.create(:preferences => { "background" => "black", "display" => large }) + # user = User.create(preferences: { "background" => "black", "display" => large }) # User.find(user.id).preferences # => { "background" => "black", "display" => large } # # You can also specify a class option as the second parameter that'll raise an exception @@ -242,7 +241,7 @@ module ActiveRecord #:nodoc: # serialize :preferences, Hash # end # - # user = User.create(:preferences => %w( one two three )) + # user = User.create(preferences: %w( one two three )) # User.find(user.id).preferences # raises SerializationTypeMismatch # # When you specify a class option, the default value for that attribute will be a new @@ -267,9 +266,9 @@ module ActiveRecord #:nodoc: # class Client < Company; end # class PriorityClient < Client; end # - # When you do <tt>Firm.create(:name => "37signals")</tt>, this record will be saved in + # When you do <tt>Firm.create(name: "37signals")</tt>, this record will be saved in # the companies table with type = "Firm". You can then fetch this row again using - # <tt>Company.where(:name => '37signals').first</tt> and it will return a Firm object. + # <tt>Company.where(name: '37signals').first</tt> and it will return a Firm object. # # If you don't have a type column defined in your table, single-table inheritance won't # be triggered. In that case, it'll work just like normal subclasses with no special magic @@ -321,8 +320,47 @@ module ActiveRecord #:nodoc: # So it's possible to assign a logger to the class through <tt>Base.logger=</tt> which will then be used by all # instances in the current object space. class Base - include ActiveRecord::Model + extend ActiveModel::Observing::ClassMethods + extend ActiveModel::Naming + + extend ActiveSupport::Benchmarkable + extend ActiveSupport::DescendantsTracker + + extend ConnectionHandling + extend QueryCache::ClassMethods + extend Querying + extend Translation + extend DynamicMatchers + extend Explain + + include Persistence + include ReadonlyAttributes + include ModelSchema + include Inheritance + include Scoping + include Sanitization + include AttributeAssignment + include ActiveModel::Conversion + include Integration + include Validations + include CounterCache + include Locking::Optimistic + include Locking::Pessimistic + include AttributeMethods + include Callbacks + include ActiveModel::Observing + include Timestamp + include Associations + include ActiveModel::SecurePassword + include AutosaveAssociation + include NestedAttributes + include Aggregations + include Transactions + include Reflection + include Serialization + include Store + include Core end -end -ActiveSupport.run_load_hooks(:active_record, ActiveRecord::Model::DeprecationProxy.new) + ActiveSupport.run_load_hooks(:active_record, Base) +end diff --git a/activerecord/lib/active_record/callbacks.rb b/activerecord/lib/active_record/callbacks.rb index 111208d0b9..1c9c627090 100644 --- a/activerecord/lib/active_record/callbacks.rb +++ b/activerecord/lib/active_record/callbacks.rb @@ -35,7 +35,7 @@ module ActiveRecord # class CreditCard < ActiveRecord::Base # # Strip everything but digits, so the user can specify "555 234 34" or # # "5552-3434" and both will mean "55523434" - # before_validation(:on => :create) do + # before_validation(on: :create) do # self.number = number.gsub(/[^0-9]/, "") if attribute_present?("number") # end # end @@ -200,6 +200,40 @@ module ActiveRecord # Callbacks are generally run in the order they are defined, with the exception of callbacks defined as # methods on the model, which are called last. # + # == Ordering callbacks + # + # Sometimes the code needs that the callbacks execute in a specific order. For example, a +before_destroy+ + # callback (+log_children+ in this case) should be executed before the children get destroyed by the +dependent: destroy+ option. + # + # Let's look at the code below: + # + # class Topic < ActiveRecord::Base + # has_many :children, dependent: destroy + # + # before_destroy :log_children + # + # private + # def log_children + # # Child processing + # end + # 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. + # + # class Topic < ActiveRecord::Base + # has_many :children, dependent: destroy + # + # before_destroy :log_children, prepend: true + # + # private + # def log_children + # # Child processing + # end + # end + # + # This way, the +before_destroy+ gets executed before the <tt>dependent: destroy</tt> is called, and the data is still available. + # # == Transactions # # The entire callback chain of a +save+, <tt>save!</tt>, or +destroy+ call runs diff --git a/activerecord/lib/active_record/coders/yaml_column.rb b/activerecord/lib/active_record/coders/yaml_column.rb index f17e7158de..f6cdc67b4d 100644 --- a/activerecord/lib/active_record/coders/yaml_column.rb +++ b/activerecord/lib/active_record/coders/yaml_column.rb @@ -1,9 +1,8 @@ require 'yaml' module ActiveRecord - # :stopdoc: - module Coders - class YAMLColumn + module Coders # :nodoc: + class YAMLColumn # :nodoc: RESCUE_ERRORS = [ ArgumentError, Psych::SyntaxError ] attr_accessor :object_class @@ -41,5 +40,4 @@ module ActiveRecord end end end - # :startdoc end diff --git a/activerecord/lib/active_record/connection_adapters/abstract/connection_pool.rb b/activerecord/lib/active_record/connection_adapters/abstract/connection_pool.rb index f42a5df75f..db0db272a6 100644 --- a/activerecord/lib/active_record/connection_adapters/abstract/connection_pool.rb +++ b/activerecord/lib/active_record/connection_adapters/abstract/connection_pool.rb @@ -1,12 +1,12 @@ require 'thread' require 'monitor' require 'set' -require 'active_support/core_ext/module/deprecation' +require 'active_support/deprecation' module ActiveRecord # Raised when a connection could not be obtained within the connection # acquisition timeout period: because max connections in pool - # are in use. + # are in use. class ConnectionTimeoutError < ConnectionNotEstablished end @@ -417,7 +417,7 @@ module ActiveRecord # queue for a connection to become available. # # Raises: - # - ConnectionTimeoutError if a connection could not be acquired + # - ConnectionTimeoutError if a connection could not be acquired def acquire_connection if conn = @available.poll conn @@ -441,11 +441,11 @@ module ActiveRecord end def new_connection - ActiveRecord::Model.send(spec.adapter_method, spec.config) + Base.send(spec.adapter_method, spec.config) end def current_connection_id #:nodoc: - ActiveRecord::Model.connection_id ||= Thread.current.object_id + Base.connection_id ||= Thread.current.object_id end def checkout_new_connection @@ -494,10 +494,18 @@ module ActiveRecord @class_to_pool = Hash.new { |h,k| h[k] = {} } end - def connection_pools + def connection_pool_list owner_to_pool.values.compact end + def connection_pools + ActiveSupport::Deprecation.warn( + "In the next release, this will return the same as #connection_pool_list. " \ + "(An array of pools, rather than a hash mapping specs to pools.)" + ) + Hash[connection_pool_list.map { |pool| [pool.spec, pool] }] + end + def establish_connection(owner, spec) @class_to_pool.clear owner_to_pool[owner] = ConnectionAdapters::ConnectionPool.new(spec) @@ -506,23 +514,23 @@ module ActiveRecord # Returns true if there are any active connections among the connection # pools that the ConnectionHandler is managing. def active_connections? - connection_pools.any?(&:active_connection?) + connection_pool_list.any?(&:active_connection?) end # Returns any connections in use by the current thread back to the pool, # and also returns connections to the pool cached by threads that are no # longer alive. def clear_active_connections! - connection_pools.each(&:release_connection) + connection_pool_list.each(&:release_connection) end # Clears the cache which maps classes. def clear_reloadable_connections! - connection_pools.each(&:clear_reloadable_connections!) + connection_pool_list.each(&:clear_reloadable_connections!) end def clear_all_connections! - connection_pools.each(&:disconnect!) + connection_pool_list.each(&:disconnect!) end # Locate the connection of the nearest super class. This can be an @@ -567,10 +575,10 @@ module ActiveRecord class_to_pool[klass] ||= begin until pool = pool_for(klass) klass = klass.superclass - break unless klass < Model::Tag + break unless klass <= Base end - class_to_pool[klass] = pool || pool_for(ActiveRecord::Model) + class_to_pool[klass] = pool end end 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 32e3c7f5d8..4f3eebce7d 100644 --- a/activerecord/lib/active_record/connection_adapters/abstract/database_statements.rb +++ b/activerecord/lib/active_record/connection_adapters/abstract/database_statements.rb @@ -150,15 +150,52 @@ module ActiveRecord # already-automatically-released savepoints: # # Model.connection.transaction do # BEGIN - # Model.connection.transaction(:requires_new => true) do # CREATE SAVEPOINT active_record_1 + # Model.connection.transaction(requires_new: true) do # CREATE SAVEPOINT active_record_1 # Model.connection.create_table(...) # # active_record_1 now automatically released # end # RELEASE SAVEPOINT active_record_1 <--- BOOM! database error! # end + # + # == Transaction isolation + # + # If your database supports setting the isolation level for a transaction, you can set + # it like so: + # + # Post.transaction(isolation: :serializable) do + # # ... + # end + # + # Valid isolation levels are: + # + # * <tt>:read_uncommitted</tt> + # * <tt>:read_committed</tt> + # * <tt>:repeatable_read</tt> + # * <tt>:serializable</tt> + # + # You should consult the documentation for your database to understand the + # semantics of these different levels: + # + # * http://www.postgresql.org/docs/9.1/static/transaction-iso.html + # * https://dev.mysql.com/doc/refman/5.0/en/set-transaction.html + # + # An <tt>ActiveRecord::TransactionIsolationError</tt> will be raised if: + # + # * The adapter does not support setting the isolation level + # * You are joining an existing open transaction + # * You are creating a nested (savepoint) transaction + # + # The mysql, mysql2 and postgresql adapters support setting the transaction + # isolation level. However, support is disabled for mysql versions below 5, + # because they are affected by a bug[http://bugs.mysql.com/bug.php?id=39170] + # which means the isolation level gets persisted outside the transaction. def transaction(options = {}) - options.assert_valid_keys :requires_new, :joinable + options.assert_valid_keys :requires_new, :joinable, :isolation if !options[:requires_new] && current_transaction.joinable? + if options[:isolation] + raise ActiveRecord::TransactionIsolationError, "cannot set isolation when joining a transaction" + end + yield else within_new_transaction(options) { yield } @@ -168,10 +205,10 @@ module ActiveRecord end def within_new_transaction(options = {}) #:nodoc: - begin_transaction(options) + transaction = begin_transaction(options) yield rescue Exception => error - rollback_transaction + rollback_transaction if transaction raise ensure begin @@ -191,9 +228,7 @@ module ActiveRecord end def begin_transaction(options = {}) #:nodoc: - @transaction = @transaction.begin - @transaction.joinable = options.fetch(:joinable, true) - @transaction + @transaction = @transaction.begin(options) end def commit_transaction #:nodoc: @@ -217,6 +252,22 @@ module ActiveRecord # Begins the transaction (and turns off auto-committing). def begin_db_transaction() end + def transaction_isolation_levels + { + read_uncommitted: "READ UNCOMMITTED", + read_committed: "READ COMMITTED", + repeatable_read: "REPEATABLE READ", + serializable: "SERIALIZABLE" + } + end + + # Begins the transaction with the isolation level set. Raises an error by + # default; adapters that support setting the isolation level should implement + # this method. + def begin_isolated_db_transaction(isolation) + raise ActiveRecord::TransactionIsolationError, "adapter does not support setting transaction isolation" + end + # Commits the transaction (and turns on auto-committing). def commit_db_transaction() end @@ -248,7 +299,7 @@ module ActiveRecord end def empty_insert_statement_value - "VALUES(DEFAULT)" + "DEFAULT VALUES" end def case_sensitive_equality_operator 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 dca355aa93..7ec6abbc45 100644 --- a/activerecord/lib/active_record/connection_adapters/abstract/schema_definitions.rb +++ b/activerecord/lib/active_record/connection_adapters/abstract/schema_definitions.rb @@ -161,21 +161,21 @@ module ActiveRecord # td.column(:granted, :boolean) # # granted BOOLEAN # - # td.column(:picture, :binary, :limit => 2.megabytes) + # td.column(:picture, :binary, limit: 2.megabytes) # # => picture BLOB(2097152) # - # td.column(:sales_stage, :string, :limit => 20, :default => 'new', :null => false) + # 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) + # 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) + # 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) + # td.column(:huge_integer, :decimal, precision: 30) # # => huge_integer DECIMAL(30) # # # Defines a column with a database-specific type. @@ -190,11 +190,11 @@ module ActiveRecord # # What can be written like this with the regular calls to column: # - # create_table "products", :force => true do |t| + # create_table "products", force: true do |t| # t.column "shop_id", :integer # t.column "creator_id", :integer - # t.column "name", :string, :default => "Untitled" - # t.column "value", :string, :default => "Untitled" + # t.column "name", :string, default: "Untitled" + # t.column "value", :string, default: "Untitled" # t.column "created_at", :datetime # t.column "updated_at", :datetime # end @@ -203,7 +203,7 @@ module ActiveRecord # # create_table :products do |t| # t.integer :shop_id, :creator_id - # t.string :name, :value, :default => "Untitled" + # t.string :name, :value, default: "Untitled" # t.timestamps # end # @@ -218,22 +218,26 @@ module ActiveRecord # create_table :taggings do |t| # t.integer :tag_id, :tagger_id, :taggable_id # t.string :tagger_type - # t.string :taggable_type, :default => 'Photo' + # t.string :taggable_type, default: 'Photo' # end - # add_index :taggings, :tag_id, :name => 'index_taggings_on_tag_id' + # add_index :taggings, :tag_id, name: 'index_taggings_on_tag_id' # add_index :taggings, [:tagger_id, :tagger_type] # # Can also be written as follows using references: # # create_table :taggings do |t| - # t.references :tag, :index => { :name => 'index_taggings_on_tag_id' } - # t.references :tagger, :polymorphic => true, :index => true - # t.references :taggable, :polymorphic => { :default => 'Photo' } + # t.references :tag, index: { name: 'index_taggings_on_tag_id' } + # t.references :tagger, polymorphic: true, index: true + # t.references :taggable, polymorphic: { default: 'Photo' } # end def column(name, type, options = {}) name = name.to_s type = type.to_sym + if primary_key_column_name == name + raise ArgumentError, "you can't redefine the primary key column '#{name}'. To define a custom primary key, pass { id: false } to create_table." + end + column = self[name] || new_column_definition(@base, name, type) limit = options.fetch(:limit) do @@ -262,7 +266,7 @@ module ActiveRecord # Adds index options to the indexes hash, keyed by column name # This is primarily used to track indexes that need to be created after the table # - # index(:account_id, :name => 'index_projects_on_account_id') + # index(:account_id, name: 'index_projects_on_account_id') def index(column_name, options = {}) indexes[column_name] = options end @@ -302,6 +306,11 @@ module ActiveRecord definition end + def primary_key_column_name + primary_key_column = columns.detect { |c| c.type == :primary_key } + primary_key_column && primary_key_column.name + end + def native @base.native_database_types end @@ -315,6 +324,7 @@ module ActiveRecord # change_table :table do |t| # t.column # t.index + # t.rename_index # t.timestamps # t.change # t.change_default @@ -365,9 +375,9 @@ module ActiveRecord # ====== Creating a simple index # t.index(:name) # ====== Creating a unique index - # t.index([:branch_id, :party_id], :unique => true) + # t.index([:branch_id, :party_id], unique: true) # ====== Creating a named index - # t.index([:branch_id, :party_id], :unique => true, :name => 'by_branch_party') + # t.index([:branch_id, :party_id], unique: true, name: 'by_branch_party') def index(column_name, options = {}) @base.add_index(@table_name, column_name, options) end @@ -377,6 +387,13 @@ module ActiveRecord @base.index_exists?(@table_name, column_name, options) end + # Renames the given index on the table. + # + # t.rename_index(:user_id, :account_id) + def rename_index(index_name, new_index_name) + @base.rename_index(@table_name, index_name, new_index_name) + end + # Adds timestamps (+created_at+ and +updated_at+) columns to the table. See SchemaStatements#add_timestamps # # t.timestamps @@ -387,7 +404,7 @@ module ActiveRecord # Changes the column's definition according to the new options. # See TableDefinition#column for details of the options you can use. # - # t.change(:name, :string, :limit => 80) + # t.change(:name, :string, limit: 80) # t.change(:description, :text) def change(column_name, type, options = {}) @base.change_column(@table_name, column_name, type, options) @@ -414,11 +431,11 @@ module ActiveRecord # ====== Remove the index_table_name_on_column in the table_name table # t.remove_index :column # ====== Remove the index named index_table_name_on_branch_id in the table_name table - # t.remove_index :column => :branch_id + # t.remove_index column: :branch_id # ====== Remove the index named index_table_name_on_branch_id_and_party_id in the table_name table - # t.remove_index :column => [:branch_id, :party_id] + # t.remove_index column: [:branch_id, :party_id] # ====== Remove the index named by_branch_party in the table_name table - # t.remove_index :name => :by_branch_party + # t.remove_index name: :by_branch_party def remove_index(options = {}) @base.remove_index(@table_name, options) end diff --git a/activerecord/lib/active_record/connection_adapters/abstract/schema_statements.rb b/activerecord/lib/active_record/connection_adapters/abstract/schema_statements.rb index 86d6266af9..f1e42dfbbe 100644 --- a/activerecord/lib/active_record/connection_adapters/abstract/schema_statements.rb +++ b/activerecord/lib/active_record/connection_adapters/abstract/schema_statements.rb @@ -36,10 +36,10 @@ module ActiveRecord # index_exists?(:suppliers, [:company_id, :company_type]) # # # Check a unique index exists - # index_exists?(:suppliers, :company_id, :unique => true) + # index_exists?(:suppliers, :company_id, unique: true) # # # Check an index with a custom name exists - # index_exists?(:suppliers, :company_id, :name => "idx_company_id" + # index_exists?(:suppliers, :company_id, name: "idx_company_id" def index_exists?(table_name, column_name, options = {}) column_names = Array(column_name) index_name = options.key?(:name) ? options[:name].to_s : index_name(table_name, :column => column_names) @@ -89,14 +89,14 @@ module ActiveRecord # # table. # # create_table(:suppliers) do |t| - # t.column :name, :string, :limit => 60 + # t.column :name, :string, limit: 60 # # Other fields here # end # # === Block form, with shorthand # # You can also use the column types as method calls, rather than calling the column method. # create_table(:suppliers) do |t| - # t.string :name, :limit => 60 + # t.string :name, limit: 60 # # Other fields here # end # @@ -104,7 +104,7 @@ module ActiveRecord # # Creates a table called 'suppliers' with no columns. # create_table(:suppliers) # # Add a column to 'suppliers'. - # add_column(:suppliers, :name, :string, {:limit => 60}) + # add_column(:suppliers, :name, :string, {limit: 60}) # # The +options+ hash can include the following keys: # [<tt>:id</tt>] @@ -127,15 +127,15 @@ module ActiveRecord # Defaults to false. # # ====== Add a backend specific option to the generated SQL (MySQL) - # create_table(:suppliers, :options => 'ENGINE=InnoDB DEFAULT CHARSET=utf8') + # create_table(:suppliers, options: 'ENGINE=InnoDB DEFAULT CHARSET=utf8') # generates: # CREATE TABLE suppliers ( # id int(11) DEFAULT NULL auto_increment PRIMARY KEY # ) ENGINE=InnoDB DEFAULT CHARSET=utf8 # # ====== Rename the primary key column - # create_table(:objects, :primary_key => 'guid') do |t| - # t.column :name, :string, :limit => 80 + # create_table(:objects, primary_key: 'guid') do |t| + # t.column :name, :string, limit: 80 # end # generates: # CREATE TABLE objects ( @@ -144,7 +144,7 @@ module ActiveRecord # ) # # ====== Do not add a primary key column - # create_table(:categories_suppliers, :id => false) do |t| + # create_table(:categories_suppliers, id: false) do |t| # t.column :category_id, :integer # t.column :supplier_id, :integer # end @@ -193,7 +193,7 @@ module ActiveRecord # Defaults to false. # # ====== Add a backend specific option to the generated SQL (MySQL) - # create_join_table(:assemblies, :parts, :options => 'ENGINE=InnoDB DEFAULT CHARSET=utf8') + # create_join_table(:assemblies, :parts, options: 'ENGINE=InnoDB DEFAULT CHARSET=utf8') # generates: # CREATE TABLE assemblies_parts ( # assembly_id int NOT NULL, @@ -218,7 +218,7 @@ module ActiveRecord # # # change_table() yields a Table instance # change_table(:suppliers) do |t| - # t.column :name, :string, :limit => 60 + # t.column :name, :string, limit: 60 # # Other column alterations here # end # @@ -231,12 +231,12 @@ module ActiveRecord # # ====== Add a column # change_table(:suppliers) do |t| - # t.column :name, :string, :limit => 60 + # t.column :name, :string, limit: 60 # end # # ====== Add 2 integer columns # change_table(:suppliers) do |t| - # t.integer :width, :height, :null => false, :default => 0 + # t.integer :width, :height, null: false, default: 0 # end # # ====== Add created_at/updated_at columns @@ -253,7 +253,7 @@ module ActiveRecord # # ====== Add a polymorphic foreign key column # change_table(:suppliers) do |t| - # t.belongs_to :company, :polymorphic => true + # t.belongs_to :company, polymorphic: true # end # # Creates <tt>company_type(varchar)</tt> and <tt>company_id(integer)</tt> columns @@ -318,7 +318,7 @@ module ActiveRecord # Changes the column's definition according to the new options. # See TableDefinition#column for details of the options you can use. # - # change_column(:suppliers, :name, :string, :limit => 80) + # change_column(:suppliers, :name, :string, limit: 80) # change_column(:accounts, :description, :text) def change_column(table_name, column_name, type, options = {}) raise NotImplementedError, "change_column is not implemented" @@ -352,35 +352,35 @@ module ActiveRecord # CREATE INDEX suppliers_name_index ON suppliers(name) # # ====== Creating a unique index - # add_index(:accounts, [:branch_id, :party_id], :unique => true) + # add_index(:accounts, [:branch_id, :party_id], unique: true) # generates # CREATE UNIQUE INDEX accounts_branch_id_party_id_index ON accounts(branch_id, party_id) # # ====== Creating a named index - # add_index(:accounts, [:branch_id, :party_id], :unique => true, :name => 'by_branch_party') + # add_index(:accounts, [:branch_id, :party_id], unique: true, name: 'by_branch_party') # generates # CREATE UNIQUE INDEX by_branch_party ON accounts(branch_id, party_id) # # ====== Creating an index with specific key length - # add_index(:accounts, :name, :name => 'by_name', :length => 10) + # add_index(:accounts, :name, name: 'by_name', length: 10) # generates # CREATE INDEX by_name ON accounts(name(10)) # - # add_index(:accounts, [:name, :surname], :name => 'by_name_surname', :length => {:name => 10, :surname => 15}) + # add_index(:accounts, [:name, :surname], name: 'by_name_surname', length: {name: 10, surname: 15}) # generates # CREATE INDEX by_name_surname ON accounts(name(10), surname(15)) # # Note: SQLite doesn't support index length # # ====== Creating an index with a sort order (desc or asc, asc is the default) - # add_index(:accounts, [:branch_id, :party_id, :surname], :order => {:branch_id => :desc, :party_id => :asc}) + # add_index(:accounts, [:branch_id, :party_id, :surname], order: {branch_id: :desc, party_id: :asc}) # generates # CREATE INDEX by_branch_desc_party ON accounts(branch_id DESC, party_id ASC, surname) # # Note: mysql doesn't yet support index order (it accepts the syntax but ignores it) # # ====== Creating a partial index - # add_index(:accounts, [:branch_id, :party_id], :unique => true, :where => "active") + # add_index(:accounts, [:branch_id, :party_id], unique: true, where: "active") # generates # CREATE UNIQUE INDEX index_accounts_on_branch_id_and_party_id ON accounts(branch_id, party_id) WHERE active # @@ -396,11 +396,11 @@ module ActiveRecord # Remove the index_accounts_on_column in the accounts table. # remove_index :accounts, :column # Remove the index named index_accounts_on_branch_id in the accounts table. - # remove_index :accounts, :column => :branch_id + # remove_index :accounts, column: :branch_id # Remove the index named index_accounts_on_branch_id_and_party_id in the accounts table. - # remove_index :accounts, :column => [:branch_id, :party_id] + # remove_index :accounts, column: [:branch_id, :party_id] # Remove the index named by_branch_party in the accounts table. - # remove_index :accounts, :name => :by_branch_party + # remove_index :accounts, name: :by_branch_party def remove_index(table_name, options = {}) remove_index!(table_name, index_name_for_remove(table_name, options)) end @@ -422,7 +422,7 @@ module ActiveRecord end def index_name(table_name, options) #:nodoc: - if Hash === options # legacy support + if Hash === options if options[:column] "index_#{table_name}_on_#{Array(options[:column]) * '_and_'}" elsif options[:name] @@ -540,7 +540,7 @@ module ActiveRecord column_type_sql << "(#{precision})" end elsif scale - raise ArgumentError, "Error adding decimal column: precision cannot be empty if scale if specified" + raise ArgumentError, "Error adding decimal column: precision cannot be empty if scale is specified" end elsif (type != :primary_key) && (limit ||= native.is_a?(Hash) && native[:limit]) @@ -617,15 +617,26 @@ module ActiveRecord def add_index_options(table_name, column_name, options = {}) column_names = Array(column_name) - index_name = index_name(table_name, :column => column_names) + index_name = index_name(table_name, column: column_names) if Hash === options # legacy support, since this param was a string + options.assert_valid_keys(:unique, :order, :name, :where, :length) + index_type = options[:unique] ? "UNIQUE" : "" index_name = options[:name].to_s if options.key?(:name) + if supports_partial_index? index_options = options[:where] ? " WHERE #{options[:where]}" : "" end else + if options + message = "Passing a string as third argument of `add_index` is deprecated and will" + + " be removed in Rails 4.1." + + " Use add_index(#{table_name.inspect}, #{column_name.inspect}, unique: true) instead" + + ActiveSupport::Deprecation.warn message + end + index_type = options end diff --git a/activerecord/lib/active_record/connection_adapters/abstract/transaction.rb b/activerecord/lib/active_record/connection_adapters/abstract/transaction.rb index 2117eae5cb..4cca94e40b 100644 --- a/activerecord/lib/active_record/connection_adapters/abstract/transaction.rb +++ b/activerecord/lib/active_record/connection_adapters/abstract/transaction.rb @@ -13,8 +13,8 @@ module ActiveRecord 0 end - def begin - RealTransaction.new(connection, self) + def begin(options = {}) + RealTransaction.new(connection, self, options) end def closed? @@ -38,13 +38,13 @@ module ActiveRecord attr_reader :parent, :records attr_writer :joinable - def initialize(connection, parent) + def initialize(connection, parent, options = {}) super connection @parent = parent @records = [] @finishing = false - @joinable = true + @joinable = options.fetch(:joinable, true) end # This state is necesarry so that we correctly handle stuff that might @@ -66,11 +66,11 @@ module ActiveRecord end end - def begin + def begin(options = {}) if finishing? parent.begin else - SavepointTransaction.new(connection, self) + SavepointTransaction.new(connection, self, options) end end @@ -120,9 +120,14 @@ module ActiveRecord end class RealTransaction < OpenTransaction #:nodoc: - def initialize(connection, parent) + def initialize(connection, parent, options = {}) super - connection.begin_db_transaction + + if options[:isolation] + connection.begin_isolated_db_transaction(options[:isolation]) + else + connection.begin_db_transaction + end end def perform_rollback @@ -137,7 +142,11 @@ module ActiveRecord end class SavepointTransaction < OpenTransaction #:nodoc: - def initialize(connection, parent) + def initialize(connection, parent, options = {}) + if options[:isolation] + raise ActiveRecord::TransactionIsolationError, "cannot set transaction isolation in a nested transaction" + end + super connection.create_savepoint end diff --git a/activerecord/lib/active_record/connection_adapters/abstract_adapter.rb b/activerecord/lib/active_record/connection_adapters/abstract_adapter.rb index 3a8fbcf93f..8517ce5fc5 100644 --- a/activerecord/lib/active_record/connection_adapters/abstract_adapter.rb +++ b/activerecord/lib/active_record/connection_adapters/abstract_adapter.rb @@ -3,7 +3,7 @@ require 'bigdecimal' require 'bigdecimal/util' require 'active_support/core_ext/benchmark' require 'active_record/connection_adapters/schema_cache' -require 'active_record/connection_adapters/abstract/schema_dumper' +require 'active_record/connection_adapters/abstract/schema_dumper' require 'monitor' require 'active_support/deprecation' @@ -167,6 +167,11 @@ module ActiveRecord false end + # Does this adapter support setting the isolation level for a transaction? + def supports_transaction_isolation? + false + end + # QUOTING ================================================== # Returns a bind substitution value given a +column+ and list of current @@ -258,7 +263,8 @@ module ActiveRecord end def transaction_joinable=(joinable) - ActiveSupport::Deprecation.warn "#transaction_joinable= is deprecated. Please pass the :joinable option to #begin_transaction instead." + message = "#transaction_joinable= is deprecated. Please pass the :joinable option to #begin_transaction instead." + ActiveSupport::Deprecation.warn message @transaction.joinable = joinable end diff --git a/activerecord/lib/active_record/connection_adapters/abstract_mysql_adapter.rb b/activerecord/lib/active_record/connection_adapters/abstract_mysql_adapter.rb index 1126fe7fce..84e73e6f0f 100644 --- a/activerecord/lib/active_record/connection_adapters/abstract_mysql_adapter.rb +++ b/activerecord/lib/active_record/connection_adapters/abstract_mysql_adapter.rb @@ -4,17 +4,19 @@ module ActiveRecord module ConnectionAdapters class AbstractMysqlAdapter < AbstractAdapter class Column < ConnectionAdapters::Column # :nodoc: - attr_reader :collation + attr_reader :collation, :strict - def initialize(name, default, sql_type = nil, null = true, collation = nil) - super(name, default, sql_type, null) + def initialize(name, default, sql_type = nil, null = true, collation = nil, strict = false) + @strict = strict @collation = collation + + super(name, default, sql_type, null) end def extract_default(default) if sql_type =~ /blob/i || type == :text if default.blank? - return null ? nil : '' + null || strict ? nil : '' else raise ArgumentError, "#{type} columns cannot have a default value: #{default.inspect}" end @@ -169,6 +171,14 @@ module ActiveRecord true end + # MySQL 4 technically support transaction isolation, but it is affected by a bug + # where the transaction level gets persisted for the whole session: + # + # http://bugs.mysql.com/bug.php?id=39170 + def supports_transaction_isolation? + version[0] >= 5 + end + def native_database_types NATIVE_DATABASE_TYPES end @@ -269,6 +279,13 @@ module ActiveRecord # Transactions aren't supported end + def begin_isolated_db_transaction(isolation) + execute "SET TRANSACTION ISOLATION LEVEL #{transaction_isolation_levels.fetch(isolation)}" + begin_db_transaction + rescue + # Transactions aren't supported + end + def commit_db_transaction #:nodoc: execute "COMMIT" rescue @@ -305,6 +322,10 @@ module ActiveRecord end end + def empty_insert_statement_value + "VALUES ()" + end + # SCHEMA STATEMENTS ======================================== def structure_dump #:nodoc: @@ -332,9 +353,9 @@ module ActiveRecord # Charset defaults to utf8. # # Example: - # create_database 'charset_test', :charset => 'latin1', :collation => 'latin1_bin' + # create_database 'charset_test', charset: 'latin1', collation: 'latin1_bin' # create_database 'matt_development' - # create_database 'matt_development', :charset => :big5 + # create_database 'matt_development', charset: :big5 def create_database(name, options = {}) if options[:collation] execute "CREATE DATABASE `#{name}` DEFAULT CHARACTER SET `#{options[:charset] || 'utf8'}` COLLATE `#{options[:collation]}`" @@ -477,6 +498,13 @@ module ActiveRecord # Maps logical Rails types to MySQL-specific data types. def type_to_sql(type, limit = nil, precision = nil, scale = nil) case type.to_s + when 'binary' + case limit + when 0..0xfff; "varbinary(#{limit})" + when nil; "blob" + when 0x1000..0xffffffff; "blob(#{limit})" + else raise(ActiveRecordError, "No binary type has character length #{limit}") + end when 'integer' case limit when 1; 'tinyint' @@ -548,6 +576,10 @@ module ActiveRecord where_sql end + def strict_mode? + @config.fetch(:strict, true) + end + protected # MySQL is too stupid to create a temporary table for use subquery, so we have diff --git a/activerecord/lib/active_record/connection_adapters/column.rb b/activerecord/lib/active_record/connection_adapters/column.rb index 816b5e17c1..80984f39c9 100644 --- a/activerecord/lib/active_record/connection_adapters/column.rb +++ b/activerecord/lib/active_record/connection_adapters/column.rb @@ -94,7 +94,7 @@ module ActiveRecord case type when :string, :text then value - when :integer then value.to_i + when :integer then klass.value_to_integer(value) when :float then value.to_f when :decimal then klass.value_to_decimal(value) when :datetime, :timestamp then klass.string_to_time(value) @@ -107,14 +107,15 @@ module ActiveRecord end def type_cast_code(var_name) - ActiveSupport::Deprecation.warn("Column#type_cast_code is deprecated in favor of" \ - "using Column#type_cast only, and it is going to be removed in future Rails versions.") + message = "Column#type_cast_code is deprecated in favor of using Column#type_cast only, " \ + "and it is going to be removed in future Rails versions." + ActiveSupport::Deprecation.warn message klass = self.class.name case type when :string, :text then var_name - when :integer then "(#{var_name}.to_i)" + when :integer then "#{klass}.value_to_integer(#{var_name})" when :float then "#{var_name}.to_f" when :decimal then "#{klass}.value_to_decimal(#{var_name})" when :datetime, :timestamp then "#{klass}.string_to_time(#{var_name})" @@ -197,6 +198,17 @@ module ActiveRecord end end + # Used to convert values to integer. + # handle the case when an integer column is used to store boolean values + def value_to_integer(value) + case value + when TrueClass, FalseClass + value ? 1 : 0 + else + value.to_i + end + end + # convert something to a BigDecimal def value_to_decimal(value) # Using .class is faster than .is_a? and diff --git a/activerecord/lib/active_record/connection_adapters/connection_specification.rb b/activerecord/lib/active_record/connection_adapters/connection_specification.rb index b9a61f7d91..09250d3c01 100644 --- a/activerecord/lib/active_record/connection_adapters/connection_specification.rb +++ b/activerecord/lib/active_record/connection_adapters/connection_specification.rb @@ -73,6 +73,8 @@ module ActiveRecord :database => config.path.sub(%r{^/},""), :host => config.host } spec.reject!{ |_,value| value.blank? } + uri_parser = URI::Parser.new + spec.map { |key,value| spec[key] = uri_parser.unescape(value) if value.is_a?(String) } if config.query options = Hash[config.query.split("&").map{ |pair| pair.split("=") }].symbolize_keys spec.merge!(options) diff --git a/activerecord/lib/active_record/connection_adapters/mysql2_adapter.rb b/activerecord/lib/active_record/connection_adapters/mysql2_adapter.rb index 328d080687..f55d19393c 100644 --- a/activerecord/lib/active_record/connection_adapters/mysql2_adapter.rb +++ b/activerecord/lib/active_record/connection_adapters/mysql2_adapter.rb @@ -53,7 +53,7 @@ module ActiveRecord end def new_column(field, default, type, null, collation) # :nodoc: - Column.new(field, default, type, null, collation) + Column.new(field, default, type, null, collation, strict_mode?) end def error_number(exception) @@ -175,7 +175,7 @@ module ActiveRecord # # as values. # def select_one(sql, name = nil) # result = execute(sql, name) - # result.each(:as => :hash) do |r| + # result.each(as: :hash) do |r| # return r # end # end @@ -259,9 +259,7 @@ module ActiveRecord # Make MySQL reject illegal values rather than truncating or # blanking them. See # http://dev.mysql.com/doc/refman/5.5/en/server-sql-mode.html#sqlmode_strict_all_tables - if @config.fetch(:strict, true) - variable_assignments << "SQL_MODE='STRICT_ALL_TABLES'" - end + variable_assignments << "SQL_MODE='STRICT_ALL_TABLES'" if strict_mode? encoding = @config[:encoding] diff --git a/activerecord/lib/active_record/connection_adapters/mysql_adapter.rb b/activerecord/lib/active_record/connection_adapters/mysql_adapter.rb index 0b936bbf39..e9677415cc 100644 --- a/activerecord/lib/active_record/connection_adapters/mysql_adapter.rb +++ b/activerecord/lib/active_record/connection_adapters/mysql_adapter.rb @@ -2,7 +2,7 @@ require 'active_record/connection_adapters/abstract_mysql_adapter' require 'active_record/connection_adapters/statement_pool' require 'active_support/core_ext/hash/keys' -gem 'mysql', '~> 2.8.1' +gem 'mysql', '~> 2.9' require 'mysql' class Mysql @@ -150,7 +150,7 @@ module ActiveRecord end def new_column(field, default, type, null, collation) # :nodoc: - Column.new(field, default, type, null, collation) + Column.new(field, default, type, null, collation, strict_mode?) end def error_number(exception) # :nodoc: @@ -546,9 +546,7 @@ module ActiveRecord # Make MySQL reject illegal values rather than truncating or # blanking them. See # http://dev.mysql.com/doc/refman/5.5/en/server-sql-mode.html#sqlmode_strict_all_tables - if @config.fetch(:strict, true) - execute("SET SQL_MODE='STRICT_ALL_TABLES'", :skip_logging) - end + execute("SET SQL_MODE='STRICT_ALL_TABLES'", :skip_logging) if strict_mode? end def select(sql, name = nil, binds = []) diff --git a/activerecord/lib/active_record/connection_adapters/postgresql/cast.rb b/activerecord/lib/active_record/connection_adapters/postgresql/cast.rb index 62d091357d..c04a799b8d 100644 --- a/activerecord/lib/active_record/connection_adapters/postgresql/cast.rb +++ b/activerecord/lib/active_record/connection_adapters/postgresql/cast.rb @@ -8,6 +8,8 @@ module ActiveRecord case string when 'infinity'; 1.0 / 0.0 when '-infinity'; -1.0 / 0.0 + when / BC$/ + super("-" + string.sub(/ BC$/, "")) else super 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 c8437c18cc..34d7a246b2 100644 --- a/activerecord/lib/active_record/connection_adapters/postgresql/database_statements.rb +++ b/activerecord/lib/active_record/connection_adapters/postgresql/database_statements.rb @@ -205,6 +205,11 @@ module ActiveRecord execute "BEGIN" end + def begin_isolated_db_transaction(isolation) + begin_db_transaction + execute "SET TRANSACTION ISOLATION LEVEL #{transaction_isolation_levels.fetch(isolation)}" + end + # Commits a transaction. def commit_db_transaction execute "COMMIT" @@ -216,10 +221,9 @@ module ActiveRecord end def outside_transaction? - ActiveSupport::Deprecation.warn( - "#outside_transaction? is deprecated. This method was only really used " \ - "internally, but you can use #transaction_open? instead." - ) + message = "#outside_transaction? is deprecated. This method was only really used " \ + "internally, but you can use #transaction_open? instead." + ActiveSupport::Deprecation.warn message @connection.transaction_status == PGconn::PQTRANS_IDLE end diff --git a/activerecord/lib/active_record/connection_adapters/postgresql/quoting.rb b/activerecord/lib/active_record/connection_adapters/postgresql/quoting.rb index 37d43d891d..62a4d76928 100644 --- a/activerecord/lib/active_record/connection_adapters/postgresql/quoting.rb +++ b/activerecord/lib/active_record/connection_adapters/postgresql/quoting.rb @@ -90,7 +90,7 @@ module ActiveRecord else super(value, column) end when IPAddr - return super(value, column) unless ['inet','cidr'].includes? column.sql_type + return super(value, column) unless ['inet','cidr'].include? column.sql_type PostgreSQLColumn.cidr_to_string(value) else super(value, column) @@ -129,11 +129,15 @@ module ActiveRecord # Quote date/time values for use in SQL input. Includes microseconds # if the value is a Time responding to usec. def quoted_date(value) #:nodoc: + result = super if value.acts_like?(:time) && value.respond_to?(:usec) - "#{super}.#{sprintf("%06d", value.usec)}" - else - super + result = "#{result}.#{sprintf("%06d", value.usec)}" + end + + if value.year < 0 + result = result.sub(/^-/, "") + " BC" end + result end end end diff --git a/activerecord/lib/active_record/connection_adapters/postgresql/schema_statements.rb b/activerecord/lib/active_record/connection_adapters/postgresql/schema_statements.rb index 8a073bf878..7c561b6f82 100644 --- a/activerecord/lib/active_record/connection_adapters/postgresql/schema_statements.rb +++ b/activerecord/lib/active_record/connection_adapters/postgresql/schema_statements.rb @@ -16,7 +16,7 @@ module ActiveRecord # # Example: # create_database config[:database], config - # create_database 'foo_development', :encoding => 'unicode' + # create_database 'foo_development', encoding: 'unicode' def create_database(name, options = {}) options = options.reverse_merge(:encoding => "utf8") @@ -280,16 +280,13 @@ module ActiveRecord end_sql if result.nil? or result.empty? - # If that fails, try parsing the primary key's default value. - # Support the 7.x and 8.0 nextval('foo'::text) as well as - # the 8.1+ nextval('foo'::regclass). result = query(<<-end_sql, 'SCHEMA')[0] SELECT attr.attname, CASE - WHEN split_part(def.adsrc, '''', 2) ~ '.' THEN - substr(split_part(def.adsrc, '''', 2), - strpos(split_part(def.adsrc, '''', 2), '.')+1) - ELSE split_part(def.adsrc, '''', 2) + WHEN split_part(pg_get_expr(def.adbin, def.adrelid), '''', 2) ~ '.' THEN + substr(split_part(pg_get_expr(def.adbin, def.adrelid), '''', 2), + strpos(split_part(pg_get_expr(def.adbin, def.adrelid), '''', 2), '.')+1) + ELSE split_part(pg_get_expr(def.adbin, def.adrelid), '''', 2) END FROM pg_class t JOIN pg_attribute attr ON (t.oid = attrelid) @@ -297,7 +294,7 @@ module ActiveRecord JOIN pg_constraint cons ON (conrelid = adrelid AND adnum = conkey[1]) WHERE t.oid = '#{quote_table_name(table)}'::regclass AND cons.contype = 'p' - AND def.adsrc ~* 'nextval' + AND pg_get_expr(def.adbin, def.adrelid) ~* 'nextval' end_sql end @@ -314,7 +311,7 @@ module ActiveRecord INNER JOIN pg_depend dep ON attr.attrelid = dep.refobjid AND attr.attnum = dep.refobjsubid INNER JOIN pg_constraint cons ON attr.attrelid = cons.conrelid AND attr.attnum = cons.conkey[1] WHERE cons.contype = 'p' - AND dep.refobjid = '#{table}'::regclass + AND dep.refobjid = '#{quote_table_name(table)}'::regclass end_sql row && row.first @@ -399,6 +396,13 @@ module ActiveRecord when nil, 0..0x3fffffff; super(type) else raise(ActiveRecordError, "No binary type has byte size #{limit}.") end + when 'text' + # PostgreSQL doesn't support limits on text columns. + # The hard limit is 1Gb, according to section 8.3 in the manual. + case limit + when nil, 0..0x3fffffff; super(type) + else raise(ActiveRecordError, "The limit on text can be at most 1GB - 1byte.") + end when 'integer' return 'integer' unless limit @@ -425,20 +429,17 @@ module ActiveRecord # PostgreSQL requires the ORDER BY columns in the select list for distinct queries, and # requires that the ORDER BY include the distinct column. # - # distinct("posts.id", "posts.created_at desc") + # distinct("posts.id", ["posts.created_at desc"]) + # # => "DISTINCT posts.id, posts.created_at AS alias_0" def distinct(columns, orders) #:nodoc: - return "DISTINCT #{columns}" if orders.empty? - - # Construct a clean list of column names from the ORDER BY clause, removing - # any ASC/DESC modifiers - order_columns = orders.collect do |s| - s = s.to_sql unless s.is_a?(String) - s.gsub(/\s+(ASC|DESC)\s*(NULLS\s+(FIRST|LAST)\s*)?/i, '') - end - order_columns.delete_if { |c| c.blank? } - order_columns = order_columns.zip((0...order_columns.size).to_a).map { |s,i| "#{s} AS alias_#{i}" } - - "DISTINCT #{columns}, #{order_columns * ', '}" + order_columns = orders.map{ |s| + # Convert Arel node to string + s = s.to_sql unless s.is_a?(String) + # Remove any ASC/DESC modifiers + s.gsub(/\s+(ASC|DESC)\s*(NULLS\s+(FIRST|LAST)\s*)?/i, '') + }.reject(&:blank?).map.with_index { |column, i| "#{column} AS alias_#{i}" } + + [super].concat(order_columns).join(', ') 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 761052a788..e18464fa35 100644 --- a/activerecord/lib/active_record/connection_adapters/postgresql_adapter.rb +++ b/activerecord/lib/active_record/connection_adapters/postgresql_adapter.rb @@ -78,11 +78,8 @@ module ActiveRecord when /\A\(?(-?\d+(\.\d*)?\)?)\z/ $1 # Character types - when /\A'(.*)'::(?:character varying|bpchar|text)\z/m + when /\A\(?'(.*)'::.*\b(?:character varying|bpchar|text)\z/m $1 - # Character types (8.1 formatting) - when /\AE'(.*)'::(?:character varying|bpchar|text)\z/m - $1.gsub(/\\(\d\d\d)/) { $1.oct.chr } # Binary data types when /\A'(.*)'::bytea\z/m $1 @@ -241,7 +238,7 @@ module ActiveRecord # <encoding></tt> call on the connection. # * <tt>:min_messages</tt> - An optional client min messages that is used in a # <tt>SET client_min_messages TO <min_messages></tt> call on the connection. - # * <tt>:insert_returning</tt> - An optional boolean to control the use or <tt>RETURNING</tt> for <tt>INSERT<tt> statements + # * <tt>:insert_returning</tt> - An optional boolean to control the use or <tt>RETURNING</tt> for <tt>INSERT</tt> statements # defaults to true. # # Any further options are used as connection parameters to libpq. See @@ -370,6 +367,10 @@ module ActiveRecord true end + def supports_transaction_isolation? + true + end + class StatementPool < ConnectionAdapters::StatementPool def initialize(connection, max) super @@ -759,7 +760,8 @@ module ActiveRecord # - ::regclass is a function that gives the id for a table name def column_definitions(table_name) #:nodoc: exec_query(<<-end_sql, 'SCHEMA').rows - SELECT a.attname, format_type(a.atttypid, a.atttypmod), d.adsrc, a.attnotnull, a.atttypid, a.atttypmod + SELECT a.attname, format_type(a.atttypid, a.atttypmod), + pg_get_expr(d.adbin, d.adrelid), a.attnotnull, a.atttypid, a.atttypmod FROM pg_attribute a LEFT JOIN pg_attrdef d ON a.attrelid = d.adrelid AND a.attnum = d.adnum WHERE a.attrelid = '#{quote_table_name(table_name)}'::regclass diff --git a/activerecord/lib/active_record/connection_adapters/sqlite3_adapter.rb b/activerecord/lib/active_record/connection_adapters/sqlite3_adapter.rb index 4a48812807..b89e9a01a8 100644 --- a/activerecord/lib/active_record/connection_adapters/sqlite3_adapter.rb +++ b/activerecord/lib/active_record/connection_adapters/sqlite3_adapter.rb @@ -251,7 +251,7 @@ module ActiveRecord value = super if column.type == :string && value.encoding == Encoding::ASCII_8BIT logger.error "Binary data inserted for `string` type on column `#{column.name}`" if logger - value.encode! 'utf-8' + value = value.encode Encoding::UTF_8 end value end @@ -490,10 +490,6 @@ module ActiveRecord alter_table(table_name, :rename => {column_name.to_s => new_column_name.to_s}) end - def empty_insert_statement_value - "VALUES(NULL)" - end - protected def select(sql, name = nil, binds = []) #:nodoc: exec_query(sql, name, binds) @@ -523,24 +519,22 @@ module ActiveRecord def copy_table(from, to, options = {}) #:nodoc: from_primary_key = primary_key(from) - options[:primary_key] = from_primary_key if from_primary_key != 'id' - unless options[:primary_key] - options[:id] = columns(from).detect{|c| c.name == 'id'}.present? && from_primary_key == 'id' - end + options[:id] = false create_table(to, options) do |definition| @definition = definition + @definition.primary_key(from_primary_key) if from_primary_key.present? columns(from).each do |column| column_name = options[:rename] ? (options[:rename][column.name] || options[:rename][column.name.to_sym] || column.name) : column.name + next if column_name == from_primary_key @definition.column(column_name, column.type, :limit => column.limit, :default => column.default, :precision => column.precision, :scale => column.scale, :null => column.null) end - @definition.primary_key(from_primary_key) if from_primary_key yield @definition if block_given? end diff --git a/activerecord/lib/active_record/connection_handling.rb b/activerecord/lib/active_record/connection_handling.rb index 3531be05bf..d6d998c7be 100644 --- a/activerecord/lib/active_record/connection_handling.rb +++ b/activerecord/lib/active_record/connection_handling.rb @@ -1,4 +1,3 @@ - module ActiveRecord module ConnectionHandling # Establishes the connection to the database. Accepts a hash as input where @@ -6,18 +5,18 @@ module ActiveRecord # example for regular databases (MySQL, Postgresql, etc): # # ActiveRecord::Base.establish_connection( - # :adapter => "mysql", - # :host => "localhost", - # :username => "myuser", - # :password => "mypass", - # :database => "somedatabase" + # adapter: "mysql", + # host: "localhost", + # username: "myuser", + # password: "mypass", + # database: "somedatabase" # ) # # Example for SQLite database: # # ActiveRecord::Base.establish_connection( - # :adapter => "sqlite", - # :database => "path/to/dbfile" + # adapter: "sqlite", + # database: "path/to/dbfile" # ) # # Also accepts keys as strings (for parsing from YAML for example): @@ -65,7 +64,7 @@ module ActiveRecord # Returns the configuration of the associated connection as a hash: # # ActiveRecord::Base.connection_config - # # => {:pool=>5, :timeout=>5000, :database=>"db/development.sqlite3", :adapter=>"sqlite3"} + # # => {pool: 5, timeout: 5000, database: "db/development.sqlite3", adapter: "sqlite3"} # # Please use only for reading. def connection_config diff --git a/activerecord/lib/active_record/core.rb b/activerecord/lib/active_record/core.rb index f97c363871..957027c1ee 100644 --- a/activerecord/lib/active_record/core.rb +++ b/activerecord/lib/active_record/core.rb @@ -3,88 +3,74 @@ require 'active_support/core_ext/object/duplicable' require 'thread' module ActiveRecord - ActiveSupport.on_load(:active_record_config) do - ## - # :singleton-method: - # - # Accepts a logger conforming to the interface of Log4r which is then - # passed on to any new database connections made and which can be - # retrieved on both a class and instance level by calling +logger+. - mattr_accessor :logger, instance_accessor: false - - ## - # :singleton-method: - # Contains the database configuration - as is typically stored in config/database.yml - - # as a Hash. - # - # For example, the following database.yml... - # - # development: - # adapter: sqlite3 - # database: db/development.sqlite3 - # - # production: - # adapter: sqlite3 - # database: db/production.sqlite3 - # - # ...would result in ActiveRecord::Base.configurations to look like this: - # - # { - # 'development' => { - # 'adapter' => 'sqlite3', - # 'database' => 'db/development.sqlite3' - # }, - # 'production' => { - # 'adapter' => 'sqlite3', - # 'database' => 'db/production.sqlite3' - # } - # } - mattr_accessor :configurations, instance_accessor: false - self.configurations = {} - - ## - # :singleton-method: - # Determines whether to use Time.utc (using :utc) or Time.local (using :local) when pulling - # dates and times from the database. This is set to :utc by default. - mattr_accessor :default_timezone, instance_accessor: false - self.default_timezone = :utc - - ## - # :singleton-method: - # Specifies the format to use when dumping the database schema with Rails' - # Rakefile. If :sql, the schema is dumped as (potentially database- - # specific) SQL statements. If :ruby, the schema is dumped as an - # ActiveRecord::Schema file which can be loaded into any database that - # supports migrations. Use :ruby if you want to have different database - # adapters for, e.g., your development and test environments. - mattr_accessor :schema_format, instance_accessor: false - self.schema_format = :ruby + module Core + extend ActiveSupport::Concern - ## - # :singleton-method: - # Specify whether or not to use timestamps for migration versions - mattr_accessor :timestamped_migrations, instance_accessor: false - self.timestamped_migrations = true + included do + ## + # :singleton-method: + # + # Accepts a logger conforming to the interface of Log4r which is then + # passed on to any new database connections made and which can be + # retrieved on both a class and instance level by calling +logger+. + mattr_accessor :logger, instance_writer: false - mattr_accessor :connection_handler, instance_accessor: false - self.connection_handler = ConnectionAdapters::ConnectionHandler.new + ## + # :singleton-method: + # Contains the database configuration - as is typically stored in config/database.yml - + # as a Hash. + # + # For example, the following database.yml... + # + # development: + # adapter: sqlite3 + # database: db/development.sqlite3 + # + # production: + # adapter: sqlite3 + # database: db/production.sqlite3 + # + # ...would result in ActiveRecord::Base.configurations to look like this: + # + # { + # 'development' => { + # 'adapter' => 'sqlite3', + # 'database' => 'db/development.sqlite3' + # }, + # 'production' => { + # 'adapter' => 'sqlite3', + # 'database' => 'db/production.sqlite3' + # } + # } + mattr_accessor :configurations, instance_writer: false + self.configurations = {} - mattr_accessor :dependent_restrict_raises, instance_accessor: false - self.dependent_restrict_raises = true - end + ## + # :singleton-method: + # Determines whether to use Time.utc (using :utc) or Time.local (using :local) when pulling + # dates and times from the database. This is set to :utc by default. + mattr_accessor :default_timezone, instance_writer: false + self.default_timezone = :utc - module Core - extend ActiveSupport::Concern + ## + # :singleton-method: + # Specifies the format to use when dumping the database schema with Rails' + # Rakefile. If :sql, the schema is dumped as (potentially database- + # specific) SQL statements. If :ruby, the schema is dumped as an + # ActiveRecord::Schema file which can be loaded into any database that + # supports migrations. Use :ruby if you want to have different database + # adapters for, e.g., your development and test environments. + mattr_accessor :schema_format, instance_writer: false + self.schema_format = :ruby - included do ## # :singleton-method: - # The connection handler - config_attribute :connection_handler + # Specify whether or not to use timestamps for migration versions + mattr_accessor :timestamped_migrations, instance_writer: false + self.timestamped_migrations = true - %w(logger configurations default_timezone schema_format timestamped_migrations).each do |name| - config_attribute name, global: true - end + class_attribute :connection_handler, instance_writer: false + self.connection_handler = ConnectionAdapters::ConnectionHandler.new end module ClassMethods @@ -139,7 +125,13 @@ module ActiveRecord # Returns the Arel engine. def arel_engine - @arel_engine ||= connection_handler.retrieve_connection_pool(self) ? self : active_record_super.arel_engine + @arel_engine ||= begin + if Base == self || connection_handler.retrieve_connection_pool(self) + self + else + superclass.arel_engine + end + end end private @@ -162,7 +154,7 @@ module ActiveRecord # # ==== Example: # # Instantiates a single new object - # User.new(:first_name => 'Jamie') + # User.new(first_name: 'Jamie') def initialize(attributes = nil) defaults = self.class.column_defaults.dup defaults.each { |k, v| defaults[k] = v.dup if v.duplicable? } diff --git a/activerecord/lib/active_record/counter_cache.rb b/activerecord/lib/active_record/counter_cache.rb index c877079b25..c53b7b3e78 100644 --- a/activerecord/lib/active_record/counter_cache.rb +++ b/activerecord/lib/active_record/counter_cache.rb @@ -22,6 +22,10 @@ module ActiveRecord counters.each do |association| has_many_association = reflect_on_association(association.to_sym) + if has_many_association.is_a? ActiveRecord::Reflection::ThroughReflection + has_many_association = has_many_association.through_reflection + end + foreign_key = has_many_association.foreign_key.to_s child_class = has_many_association.klass belongs_to = child_class.reflect_on_all_associations(:belongs_to) @@ -52,7 +56,7 @@ module ActiveRecord # # # For the Post with id of 5, decrement the comment_count by 1, and # # increment the action_count by 1 - # Post.update_counters 5, :comment_count => -1, :action_count => 1 + # Post.update_counters 5, comment_count: -1, action_count: 1 # # Executes the following SQL: # # UPDATE posts # # SET comment_count = COALESCE(comment_count, 0) - 1, @@ -60,7 +64,7 @@ module ActiveRecord # # WHERE id = 5 # # # For the Posts with id of 10 and 15, increment the comment_count by 1 - # Post.update_counters [10, 15], :comment_count => 1 + # Post.update_counters [10, 15], comment_count: 1 # # Executes the following SQL: # # UPDATE posts # # SET comment_count = COALESCE(comment_count, 0) + 1 diff --git a/activerecord/lib/active_record/errors.rb b/activerecord/lib/active_record/errors.rb index 5f157fde6d..c615d59725 100644 --- a/activerecord/lib/active_record/errors.rb +++ b/activerecord/lib/active_record/errors.rb @@ -22,7 +22,7 @@ module ActiveRecord # end # # # Comments are not patches, this assignment raises AssociationTypeMismatch. - # @ticket.patches << Comment.new(:content => "Please attach tests to your patch.") + # @ticket.patches << Comment.new(content: "Please attach tests to your patch.") class AssociationTypeMismatch < ActiveRecordError end @@ -193,6 +193,20 @@ module ActiveRecord end + # Raised when a relation cannot be mutated because it's already loaded. + # + # class Task < ActiveRecord::Base + # end + # + # relation = Task.all + # relation.loaded? # => true + # + # # Methods which try to mutate a loaded relation fail. + # relation.where!(title: 'TODO') # => ActiveRecord::ImmutableRelation + # relation.limit!(5) # => ActiveRecord::ImmutableRelation class ImmutableRelation < ActiveRecordError end + + class TransactionIsolationError < ActiveRecordError + end end diff --git a/activerecord/lib/active_record/explain.rb b/activerecord/lib/active_record/explain.rb index 9e0390bed1..af772996f1 100644 --- a/activerecord/lib/active_record/explain.rb +++ b/activerecord/lib/active_record/explain.rb @@ -1,12 +1,10 @@ require 'active_support/lazy_load_hooks' module ActiveRecord - ActiveSupport.on_load(:active_record_config) do - mattr_accessor :auto_explain_threshold_in_seconds, instance_accessor: false - end - module Explain - delegate :auto_explain_threshold_in_seconds, :auto_explain_threshold_in_seconds=, to: 'ActiveRecord::Model' + def self.extended(base) + base.mattr_accessor :auto_explain_threshold_in_seconds, instance_accessor: false + end # If auto explain is enabled, this method triggers EXPLAIN logging for the # queries triggered by the block if it takes more than the threshold as a diff --git a/activerecord/lib/active_record/fixtures/file.rb b/activerecord/lib/active_record/fixture_set/file.rb index a9cabf5a7b..11b53275e1 100644 --- a/activerecord/lib/active_record/fixtures/file.rb +++ b/activerecord/lib/active_record/fixture_set/file.rb @@ -2,8 +2,8 @@ require 'erb' require 'yaml' module ActiveRecord - class Fixtures - class File + class FixtureSet + class File # :nodoc: include Enumerable ## diff --git a/activerecord/lib/active_record/fixtures.rb b/activerecord/lib/active_record/fixtures.rb index 60fc653735..7922bbcfa0 100644 --- a/activerecord/lib/active_record/fixtures.rb +++ b/activerecord/lib/active_record/fixtures.rb @@ -2,9 +2,11 @@ require 'erb' require 'yaml' require 'zlib' require 'active_support/dependencies' -require 'active_record/fixtures/file' +require 'active_record/fixture_set/file' require 'active_record/errors' +require 'active_support/deprecation' # temporary + module ActiveRecord class FixtureClassNotFound < ActiveRecord::ActiveRecordError #:nodoc: end @@ -248,7 +250,7 @@ module ActiveRecord # # ### in fruit.rb # - # belongs_to :eater, :polymorphic => true + # belongs_to :eater, polymorphic: true # # ### in fruits.yml # @@ -350,8 +352,8 @@ module ActiveRecord # to the rescue: # # george_reginald: - # monkey_id: <%= ActiveRecord::Fixtures.identify(:reginald) %> - # pirate_id: <%= ActiveRecord::Fixtures.identify(:george) %> + # monkey_id: <%= ActiveRecord::FixtureSet.identify(:reginald) %> + # pirate_id: <%= ActiveRecord::FixtureSet.identify(:george) %> # # == Support for YAML defaults # @@ -370,14 +372,21 @@ module ActiveRecord # *DEFAULTS # # Any fixture labeled "DEFAULTS" is safely ignored. - class Fixtures + class FixtureSet #-- - # NOTE: an instance of Fixtures can be called fixture_set, it is normally stored in a single YAML file and possibly in a folder with the same name. + # An instance of FixtureSet is normally stored in a single YAML file and possibly in a folder with the same name. #++ + MAX_ID = 2 ** 30 - 1 @@all_cached_fixtures = Hash.new { |h,k| h[k] = {} } + def self.find_table_name(fixture_set_name) # :nodoc: + ActiveSupport::Deprecation.warn( + "ActiveRecord::Fixtures.find_table_name is deprecated and shall be removed from future releases. Use ActiveRecord::Fixtures.default_fixture_model_name instead.") + default_fixture_model_name(fixture_set_name) + end + def self.default_fixture_model_name(fixture_set_name) # :nodoc: ActiveRecord::Base.pluralize_table_names ? fixture_set_name.singularize.camelize : @@ -451,7 +460,7 @@ module ActiveRecord fixtures_map = {} fixture_sets = files_to_read.map do |fs_name| - fixtures_map[fs_name] = new( # ActiveRecord::Fixtures.new + fixtures_map[fs_name] = new( # ActiveRecord::FixtureSet.new connection, fs_name, class_names[fs_name] || default_fixture_model_name(fs_name), @@ -550,7 +559,7 @@ module ActiveRecord rows[table_name] = fixtures.map do |label, fixture| row = fixture.to_hash - if model_class && model_class < ActiveRecord::Model + if model_class && model_class < ActiveRecord::Base # fill in timestamp columns if they aren't specified and the model is set to record_timestamps if model_class.record_timestamps timestamp_column_names.each do |c_name| @@ -565,7 +574,7 @@ module ActiveRecord # generate a primary key if necessary if has_primary_key_column? && !row.include?(primary_key_name) - row[primary_key_name] = ActiveRecord::Fixtures.identify(label) + row[primary_key_name] = ActiveRecord::FixtureSet.identify(label) end # If STI is used, find the correct subclass for association reflection @@ -588,7 +597,7 @@ module ActiveRecord row[association.foreign_type] = $1 end - row[fk_name] = ActiveRecord::Fixtures.identify(value) + row[fk_name] = ActiveRecord::FixtureSet.identify(value) end when :has_and_belongs_to_many if (targets = row.delete(association.name.to_s)) @@ -596,7 +605,7 @@ module ActiveRecord table_name = association.join_table rows[table_name].concat targets.map { |target| { association.foreign_key => row[primary_key_name], - association.association_foreign_key => ActiveRecord::Fixtures.identify(target) } + association.association_foreign_key => ActiveRecord::FixtureSet.identify(target) } } end end @@ -637,7 +646,7 @@ module ActiveRecord } + [yaml_file_path] yaml_files.each do |file| - Fixtures::File.open(file) do |fh| + FixtureSet::File.open(file) do |fh| fh.each do |fixture_name, row| fixtures[fixture_name] = ActiveRecord::Fixture.new(row, model_class) end @@ -651,6 +660,12 @@ module ActiveRecord end + #-- + # Deprecate 'Fixtures' in favor of 'FixtureSet'. + #++ + # :nodoc: + Fixtures = ActiveSupport::Deprecation::DeprecatedConstantProxy.new('ActiveRecord::Fixtures', 'ActiveRecord::FixtureSet') + class Fixture #:nodoc: include Enumerable @@ -712,7 +727,7 @@ module ActiveRecord self.pre_loaded_fixtures = false self.fixture_class_names = Hash.new do |h, fixture_set_name| - h[fixture_set_name] = ActiveRecord::Fixtures.default_fixture_model_name(fixture_set_name) + h[fixture_set_name] = ActiveRecord::FixtureSet.default_fixture_model_name(fixture_set_name) end end @@ -721,7 +736,7 @@ module ActiveRecord # # Examples: # - # set_fixture_class :some_fixture => SomeModel, + # set_fixture_class some_fixture: SomeModel, # 'namespaced/fixture' => Another::Model # # The keys must be the fixture names, that coincide with the short paths to the fixture files. @@ -847,7 +862,7 @@ module ActiveRecord end # Load fixtures for every test. else - ActiveRecord::Fixtures.reset_cache + ActiveRecord::FixtureSet.reset_cache @@already_loaded_fixtures[self.class] = nil @loaded_fixtures = load_fixtures end @@ -860,7 +875,7 @@ module ActiveRecord return unless defined?(ActiveRecord) && !ActiveRecord::Base.configurations.blank? unless run_in_transaction? - ActiveRecord::Fixtures.reset_cache + ActiveRecord::FixtureSet.reset_cache end # Rollback changes if a transaction is active. @@ -874,12 +889,12 @@ module ActiveRecord end def enlist_fixture_connections - ActiveRecord::Base.connection_handler.connection_pools.map(&:connection) + ActiveRecord::Base.connection_handler.connection_pool_list.map(&:connection) end private def load_fixtures - fixtures = ActiveRecord::Fixtures.create_fixtures(fixture_path, fixture_table_names, fixture_class_names) + fixtures = ActiveRecord::FixtureSet.create_fixtures(fixture_path, fixture_table_names, fixture_class_names) Hash[fixtures.map { |f| [f.name, f] }] end @@ -888,16 +903,16 @@ module ActiveRecord def instantiate_fixtures if pre_loaded_fixtures - raise RuntimeError, 'Load fixtures before instantiating them.' if ActiveRecord::Fixtures.all_loaded_fixtures.empty? + raise RuntimeError, 'Load fixtures before instantiating them.' if ActiveRecord::FixtureSet.all_loaded_fixtures.empty? unless @@required_fixture_classes - self.class.require_fixture_classes ActiveRecord::Fixtures.all_loaded_fixtures.keys + self.class.require_fixture_classes ActiveRecord::FixtureSet.all_loaded_fixtures.keys @@required_fixture_classes = true end - ActiveRecord::Fixtures.instantiate_all_loaded_fixtures(self, load_instances?) + ActiveRecord::FixtureSet.instantiate_all_loaded_fixtures(self, load_instances?) else raise RuntimeError, 'Load fixtures before instantiating them.' if @loaded_fixtures.nil? @loaded_fixtures.each_value do |fixture_set| - ActiveRecord::Fixtures.instantiate_fixtures(self, fixture_set, load_instances?) + ActiveRecord::FixtureSet.instantiate_fixtures(self, fixture_set, load_instances?) end end end diff --git a/activerecord/lib/active_record/inheritance.rb b/activerecord/lib/active_record/inheritance.rb index 04fff99a6e..a448fa1f5c 100644 --- a/activerecord/lib/active_record/inheritance.rb +++ b/activerecord/lib/active_record/inheritance.rb @@ -1,29 +1,22 @@ - module ActiveRecord - ActiveSupport.on_load(:active_record_config) do - # Determine whether to store the full constant name including namespace when using STI - mattr_accessor :store_full_sti_class, instance_accessor: false - self.store_full_sti_class = true - end - module Inheritance extend ActiveSupport::Concern included do - config_attribute :store_full_sti_class + # Determine whether to store the full constant name including namespace when using STI + class_attribute :store_full_sti_class, instance_writer: false + self.store_full_sti_class = true end module ClassMethods # True if this isn't a concrete subclass needing a STI type condition. def descends_from_active_record? - sup = active_record_super - - if sup.abstract_class? - sup.descends_from_active_record? - elsif self == Base + if self == Base false + elsif superclass.abstract_class? + superclass.descends_from_active_record? else - [Base, Model].include?(sup) || !columns_hash.include?(inheritance_column) + superclass == Base || !columns_hash.include?(inheritance_column) end end @@ -40,9 +33,8 @@ module ActiveRecord @symbolized_sti_name ||= sti_name.present? ? sti_name.to_sym : symbolized_base_class end - # Returns the class descending directly from ActiveRecord::Base (or - # that includes ActiveRecord::Model), or an abstract class, if any, in - # the inheritance hierarchy. + # Returns the class descending directly from ActiveRecord::Base, or + # an abstract class, if any, in the inheritance hierarchy. # # If A extends AR::Base, A.base_class will return A. If B descends from A # through some arbitrarily deep hierarchy, B.base_class will return A. @@ -50,15 +42,14 @@ module ActiveRecord # If B < A and C < B and if A is an abstract_class then both B.base_class # and C.base_class would return B as the answer since A is an abstract_class. def base_class - unless self < Model::Tag + unless self < Base raise ActiveRecordError, "#{name} doesn't belong in a hierarchy descending from ActiveRecord" end - sup = active_record_super - if sup == Base || sup == Model || sup.abstract_class? + if superclass == Base || superclass.abstract_class? self else - sup.base_class + superclass.base_class end end @@ -73,7 +64,7 @@ module ActiveRecord # class Child < SuperClass # self.table_name = 'the_table_i_really_want' # end - # + # # # <tt>self.abstract_class = true</tt> is required to make <tt>Child<.find,.create, or any Arel method></tt> use <tt>the_table_i_really_want</tt> instead of a table called <tt>super_classes</tt> # @@ -97,14 +88,6 @@ module ActiveRecord sti_class.allocate.init_with('attributes' => record, 'column_types' => column_types) end - # For internal use. - # - # If this class includes ActiveRecord::Model then it won't have a - # superclass. So this provides a way to get to the 'root' (ActiveRecord::Model). - def active_record_super #:nodoc: - superclass < Model ? superclass : Model - end - protected # Returns the class type of the record using the current module as a prefix. So descendants of diff --git a/activerecord/lib/active_record/locale/en.yml b/activerecord/lib/active_record/locale/en.yml index 896132d566..b1fbd38622 100644 --- a/activerecord/lib/active_record/locale/en.yml +++ b/activerecord/lib/active_record/locale/en.yml @@ -4,11 +4,15 @@ en: #created_at: "Created at" #updated_at: "Updated at" + # Default error messages + errors: + messages: + taken: "has already been taken" + # Active Record models configuration activerecord: errors: messages: - taken: "has already been taken" record_invalid: "Validation failed: %{errors}" restrict_dependent_destroy: one: "Cannot delete record because a dependent %{record} exists" diff --git a/activerecord/lib/active_record/locking/optimistic.rb b/activerecord/lib/active_record/locking/optimistic.rb index e96ed00f9c..035c77c424 100644 --- a/activerecord/lib/active_record/locking/optimistic.rb +++ b/activerecord/lib/active_record/locking/optimistic.rb @@ -1,9 +1,4 @@ module ActiveRecord - ActiveSupport.on_load(:active_record_config) do - mattr_accessor :lock_optimistically, instance_accessor: false - self.lock_optimistically = true - end - module Locking # == What is Optimistic Locking # @@ -56,7 +51,8 @@ module ActiveRecord extend ActiveSupport::Concern included do - config_attribute :lock_optimistically + class_attribute :lock_optimistically, instance_writer: false + self.lock_optimistically = true end def locking_enabled? #:nodoc: diff --git a/activerecord/lib/active_record/locking/pessimistic.rb b/activerecord/lib/active_record/locking/pessimistic.rb index 58af92f0b1..b4bb95a392 100644 --- a/activerecord/lib/active_record/locking/pessimistic.rb +++ b/activerecord/lib/active_record/locking/pessimistic.rb @@ -3,12 +3,12 @@ module ActiveRecord # Locking::Pessimistic provides support for row-level locking using # SELECT ... FOR UPDATE and other lock types. # - # Pass <tt>:lock => true</tt> to <tt>ActiveRecord::Base.find</tt> to obtain an exclusive + # Pass <tt>lock: true</tt> to <tt>ActiveRecord::Base.find</tt> to obtain an exclusive # lock on the selected rows: # # select * from accounts where id=1 for update - # Account.find(1, :lock => true) + # Account.find(1, lock: true) # - # Pass <tt>:lock => 'some locking clause'</tt> to give a database-specific locking clause + # Pass <tt>lock: 'some locking clause'</tt> to give a database-specific locking clause # of your own such as 'LOCK IN SHARE MODE' or 'FOR UPDATE NOWAIT'. Example: # # Account.transaction do diff --git a/activerecord/lib/active_record/log_subscriber.rb b/activerecord/lib/active_record/log_subscriber.rb index a25f2c7bca..ca79950049 100644 --- a/activerecord/lib/active_record/log_subscriber.rb +++ b/activerecord/lib/active_record/log_subscriber.rb @@ -1,11 +1,13 @@ module ActiveRecord class LogSubscriber < ActiveSupport::LogSubscriber + IGNORE_PAYLOAD_NAMES = ["SCHEMA", "EXPLAIN"] + def self.runtime=(value) - Thread.current["active_record_sql_runtime"] = value + Thread.current[:active_record_sql_runtime] = value end def self.runtime - Thread.current["active_record_sql_runtime"] ||= 0 + Thread.current[:active_record_sql_runtime] ||= 0 end def self.reset_runtime @@ -24,9 +26,9 @@ module ActiveRecord payload = event.payload - return if 'SCHEMA' == payload[:name] + return if IGNORE_PAYLOAD_NAMES.include?(payload[:name]) - name = '%s (%.1fms)' % [payload[:name], event.duration] + name = "#{payload[:name]} (#{event.duration.round(1)}ms)" sql = payload[:sql].squeeze(' ') binds = nil diff --git a/activerecord/lib/active_record/migration.rb b/activerecord/lib/active_record/migration.rb index c1d57855a9..22347fcaef 100644 --- a/activerecord/lib/active_record/migration.rb +++ b/activerecord/lib/active_record/migration.rb @@ -32,7 +32,7 @@ module ActiveRecord class PendingMigrationError < ActiveRecordError#:nodoc: def initialize - super("Migrations are pending run 'rake db:migrate RAILS_ENV=#{ENV['RAILS_ENV']}' to resolve the issue") + super("Migrations are pending; run 'rake db:migrate RAILS_ENV=#{ENV['RAILS_ENV']}' to resolve this issue.") end end @@ -50,7 +50,7 @@ module ActiveRecord # # class AddSsl < ActiveRecord::Migration # def up - # add_column :accounts, :ssl_enabled, :boolean, :default => true + # add_column :accounts, :ssl_enabled, :boolean, default: true # end # # def down @@ -62,7 +62,7 @@ module ActiveRecord # if you're backing out of the migration. It shows how all migrations have # two methods +up+ and +down+ that describes the transformations # required to implement or remove the migration. These methods can consist - # of both the migration specific methods like add_column and remove_column, + # of both the migration specific methods like +add_column+ and +remove_column+, # but may also contain regular Ruby code for generating data needed for the # transformations. # @@ -78,9 +78,9 @@ module ActiveRecord # t.integer :position # end # - # SystemSetting.create :name => "notice", - # :label => "Use notice?", - # :value => 1 + # SystemSetting.create name: 'notice', + # label: 'Use notice?', + # value: 1 # end # # def down @@ -88,19 +88,22 @@ module ActiveRecord # end # end # - # This migration first adds the system_settings table, then creates the very + # This migration first adds the +system_settings+ table, then creates the very # first row in it using the Active Record model that relies on the table. It - # also uses the more advanced create_table syntax where you can specify a + # also uses the more advanced +create_table+ syntax where you can specify a # complete table schema in one block call. # # == Available transformations # - # * <tt>create_table(name, options)</tt> Creates a table called +name+ and + # * <tt>create_table(name, options)</tt>: Creates a table called +name+ and # makes the table object available to a block that can then add columns to it, - # following the same format as add_column. See example above. The options hash + # 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 availabe 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 @@ -109,9 +112,9 @@ module ActiveRecord # <tt>:string</tt>, <tt>:text</tt>, <tt>:integer</tt>, <tt>:float</tt>, # <tt>:decimal</tt>, <tt>:datetime</tt>, <tt>:timestamp</tt>, <tt>:time</tt>, # <tt>:date</tt>, <tt>:binary</tt>, <tt>:boolean</tt>. A default value can be - # specified by passing an +options+ hash like <tt>{ :default => 11 }</tt>. + # specified by passing an +options+ hash like <tt>{ default: 11 }</tt>. # Other options include <tt>:limit</tt> and <tt>:null</tt> (e.g. - # <tt>{ :limit => 50, :null => false }</tt>) -- see + # <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. @@ -122,11 +125,11 @@ module ActiveRecord # * <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. { :order => {:name => :desc} }</tt>). - # * <tt>remove_index(table_name, :column => column_name)</tt>: Removes the index + # <tt>{ name: 'users_name_index', unique: true }</tt>) and <tt>:order</tt> + # (e.g. <tt>{ order: { name: :desc } }</tt>). + # * <tt>remove_index(table_name, column: column_name)</tt>: Removes the index # specified by +column_name+. - # * <tt>remove_index(table_name, :name => index_name)</tt>: Removes the index + # * <tt>remove_index(table_name, name: index_name)</tt>: Removes the index # specified by +index_name+. # # == Irreversible transformations @@ -339,7 +342,9 @@ module ActiveRecord end def call(env) - ActiveRecord::Migration.check_pending! + ActiveRecord::Base.logger.quietly do + ActiveRecord::Migration.check_pending! + end @app.call(env) end end @@ -637,7 +642,11 @@ module ActiveRecord def proper_table_name(name) # Use the Active Record objects own table_name, or pre/suffix from ActiveRecord::Base if name is a symbol/string - name.table_name rescue "#{ActiveRecord::Base.table_name_prefix}#{name}#{ActiveRecord::Base.table_name_suffix}" + if name.respond_to? :table_name + name.table_name + else + "#{ActiveRecord::Base.table_name_prefix}#{name}#{ActiveRecord::Base.table_name_suffix}" + end end def migrations_paths @@ -727,9 +736,8 @@ module ActiveRecord running = runnable if block_given? - ActiveSupport::Deprecation.warn(<<-eomsg) -block argument to migrate is deprecated, please filter migrations before constructing the migrator - eomsg + message = "block argument to migrate is deprecated, please filter migrations before constructing the migrator" + ActiveSupport::Deprecation.warn message running.select! { |m| yield m } end diff --git a/activerecord/lib/active_record/model.rb b/activerecord/lib/active_record/model.rb deleted file mode 100644 index 44cde49bd5..0000000000 --- a/activerecord/lib/active_record/model.rb +++ /dev/null @@ -1,170 +0,0 @@ -require 'active_support/core_ext/module/attribute_accessors' - -module ActiveRecord - module Configuration # :nodoc: - # This just abstracts out how we define configuration options in AR. Essentially we - # have mattr_accessors on the ActiveRecord:Model constant that define global defaults. - # Classes that then use AR get class_attributes defined, which means that when they - # are assigned the default will be overridden for that class and subclasses. (Except - # when options[:global] == true, in which case there is one global value always.) - def config_attribute(name, options = {}) - if options[:global] - class_eval <<-CODE, __FILE__, __LINE__ + 1 - def self.#{name}; ActiveRecord::Model.#{name}; end - def #{name}; ActiveRecord::Model.#{name}; end - def self.#{name}=(val); ActiveRecord::Model.#{name} = val; end - CODE - else - options[:instance_writer] ||= false - class_attribute name, options - - singleton_class.class_eval <<-CODE, __FILE__, __LINE__ + 1 - remove_method :#{name} - def #{name}; ActiveRecord::Model.#{name}; end - CODE - end - end - end - - # <tt>ActiveRecord::Model</tt> can be included into a class to add Active Record persistence. - # This is an alternative to inheriting from <tt>ActiveRecord::Base</tt>. Example: - # - # class Post - # include ActiveRecord::Model - # end - # - module Model - extend ActiveSupport::Concern - extend ConnectionHandling - extend ActiveModel::Observing::ClassMethods - - # This allows us to detect an ActiveRecord::Model while it's in the process of being included. - module Tag; end - - def self.append_features(base) - base.class_eval do - include Tag - extend Configuration - end - - super - end - - included do - extend ActiveModel::Naming - extend ActiveSupport::Benchmarkable - extend ActiveSupport::DescendantsTracker - - extend QueryCache::ClassMethods - extend Querying - extend Translation - extend DynamicMatchers - extend Explain - extend ConnectionHandling - - initialize_generated_modules unless self == Base - end - - include Persistence - include ReadonlyAttributes - include ModelSchema - include Inheritance - include Scoping - include Sanitization - include AttributeAssignment - include ActiveModel::Conversion - include Integration - include Validations - include CounterCache - include Locking::Optimistic - include Locking::Pessimistic - include AttributeMethods - include Callbacks - include ActiveModel::Observing - include Timestamp - include Associations - include ActiveModel::SecurePassword - include AutosaveAssociation - include NestedAttributes - include Aggregations - include Transactions - include Reflection - include Serialization - include Store - include Core - - class << self - def arel_engine - self - end - - def abstract_class? - false - end - - # Defines the name of the table column which will store the class name on single-table - # inheritance situations. - # - # The default inheritance column name is +type+, which means it's a - # reserved word inside Active Record. To be able to use single-table - # inheritance with another column name, or to use the column +type+ in - # your own model for something else, you can override this method to - # return a different name: - # - # def self.inheritance_column - # 'zoink' - # end - def inheritance_column - 'type' - end - end - - class DeprecationProxy < BasicObject #:nodoc: - def initialize(model = Model, base = Base) - @model = model - @base = base - end - - def method_missing(name, *args, &block) - if @model.respond_to?(name, true) - @model.send(name, *args, &block) - else - ::ActiveSupport::Deprecation.warn( - "The object passed to the active_record load hook was previously ActiveRecord::Base " \ - "(a Class). Now it is ActiveRecord::Model (a Module). You have called `#{name}' which " \ - "is only defined on ActiveRecord::Base. Please change your code so that it works with " \ - "a module rather than a class. (Model is included in Base, so anything added to Model " \ - "will be available on Base as well.)" - ) - @base.send(name, *args, &block) - end - end - - alias send method_missing - - def extend(*mods) - ::ActiveSupport::Deprecation.warn( - "The object passed to the active_record load hook was previously ActiveRecord::Base " \ - "(a Class). Now it is ActiveRecord::Model (a Module). You have called `extend' which " \ - "would add singleton methods to Model. This is presumably not what you want, since the " \ - "methods would not be inherited down to Base. Rather than using extend, please use " \ - "ActiveSupport::Concern + include, which will ensure that your class methods are " \ - "inherited." - ) - @base.extend(*mods) - end - end - end - - # This hook is where config accessors on Model get defined. - # - # We don't want to just open the Model module and add stuff to it in other files, because - # that would cause Model to load, which causes all sorts of loading order issues. - # - # We need this hook rather than just using the :active_record one, because users of the - # :active_record hook may need to use config options. - ActiveSupport.run_load_hooks(:active_record_config, Model) - - # Load Base at this point, because the active_record load hook is run in that file. - Base -end diff --git a/activerecord/lib/active_record/model_schema.rb b/activerecord/lib/active_record/model_schema.rb index 99de16cd33..628ab0f566 100644 --- a/activerecord/lib/active_record/model_schema.rb +++ b/activerecord/lib/active_record/model_schema.rb @@ -1,18 +1,4 @@ - module ActiveRecord - ActiveSupport.on_load(:active_record_config) do - mattr_accessor :primary_key_prefix_type, instance_accessor: false - - mattr_accessor :table_name_prefix, instance_accessor: false - self.table_name_prefix = "" - - mattr_accessor :table_name_suffix, instance_accessor: false - self.table_name_suffix = "" - - mattr_accessor :pluralize_table_names, instance_accessor: false - self.pluralize_table_names = true - end - module ModelSchema extend ActiveSupport::Concern @@ -24,7 +10,7 @@ module ActiveRecord # the Product class will look for "productid" instead of "id" as the primary column. If the # latter is specified, the Product class will look for "product_id" instead of "id". Remember # that this is a global setting for all Active Records. - config_attribute :primary_key_prefix_type, global: true + mattr_accessor :primary_key_prefix_type, instance_writer: false ## # :singleton-method: @@ -36,20 +22,25 @@ module ActiveRecord # If you are organising your models within modules you can add a prefix to the models within # a namespace by defining a singleton method in the parent module called table_name_prefix which # returns your chosen prefix. - config_attribute :table_name_prefix + class_attribute :table_name_prefix, instance_writer: false + self.table_name_prefix = "" ## # :singleton-method: # Works like +table_name_prefix+, but appends instead of prepends (set to "_basecamp" gives "projects_basecamp", # "people_basecamp"). By default, the suffix is the empty string. - config_attribute :table_name_suffix + class_attribute :table_name_suffix, instance_writer: false + self.table_name_suffix = "" ## # :singleton-method: # Indicates whether table names should be the pluralized versions of the corresponding class names. # If true, the default table name for a Product class will be +products+. If false, it would just be +product+. # See table_name for the full rules on table/class naming. This is true, by default. - config_attribute :pluralize_table_names + class_attribute :pluralize_table_names, instance_writer: false + self.pluralize_table_names = true + + self.inheritance_column = 'type' end module ClassMethods @@ -144,9 +135,9 @@ module ActiveRecord # Computes the table name, (re)sets it internally, and returns it. def reset_table_name #:nodoc: self.table_name = if abstract_class? - active_record_super == Base ? nil : active_record_super.table_name - elsif active_record_super.abstract_class? - active_record_super.table_name || compute_table_name + superclass == Base ? nil : superclass.table_name + elsif superclass.abstract_class? + superclass.table_name || compute_table_name else compute_table_name end @@ -156,9 +147,17 @@ module ActiveRecord (parents.detect{ |p| p.respond_to?(:table_name_prefix) } || self).table_name_prefix end - # The name of the column containing the object's class when Single Table Inheritance is used + # Defines the name of the table column which will store the class name on single-table + # inheritance situations. + # + # The default inheritance column name is +type+, which means it's a + # reserved word inside Active Record. To be able to use single-table + # inheritance with another column name, or to use the column +type+ in + # your own model for something else, you can set +inheritance_column+: + # + # self.inheritance_column = 'zoink' def inheritance_column - (@inheritance_column ||= nil) || active_record_super.inheritance_column + (@inheritance_column ||= nil) || superclass.inheritance_column end # Sets the value of inheritance_column @@ -286,7 +285,7 @@ module ActiveRecord # # JobLevel.reset_column_information # %w{assistant executive manager director}.each do |type| - # JobLevel.create(:name => type) + # JobLevel.create(name: type) # end # end # @@ -331,7 +330,7 @@ module ActiveRecord base = base_class if self == base # Nested classes are prefixed with singular parent table name. - if parent < ActiveRecord::Model && !parent.abstract_class? + if parent < Base && !parent.abstract_class? contained = parent.table_name contained = contained.singularize if parent.pluralize_table_names contained += '_' diff --git a/activerecord/lib/active_record/nested_attributes.rb b/activerecord/lib/active_record/nested_attributes.rb index 2e7fb3bbb3..4c9bd76d7c 100644 --- a/activerecord/lib/active_record/nested_attributes.rb +++ b/activerecord/lib/active_record/nested_attributes.rb @@ -3,11 +3,6 @@ require 'active_support/core_ext/object/try' require 'active_support/core_ext/hash/indifferent_access' module ActiveRecord - ActiveSupport.on_load(:active_record_config) do - mattr_accessor :nested_attributes_options, instance_accessor: false - self.nested_attributes_options = {} - end - module NestedAttributes #:nodoc: class TooManyRecords < ActiveRecordError end @@ -15,7 +10,8 @@ module ActiveRecord extend ActiveSupport::Concern included do - config_attribute :nested_attributes_options + class_attribute :nested_attributes_options, instance_writer: false + self.nested_attributes_options = {} end # = Active Record Nested Attributes @@ -54,14 +50,14 @@ module ActiveRecord # Enabling nested attributes on a one-to-one association allows you to # create the member and avatar in one go: # - # params = { :member => { :name => 'Jack', :avatar_attributes => { :icon => 'smiling' } } } + # params = { member: { name: 'Jack', avatar_attributes: { icon: 'smiling' } } } # member = Member.create(params[:member]) # member.avatar.id # => 2 # member.avatar.icon # => 'smiling' # # It also allows you to update the avatar through the member: # - # params = { :member => { :avatar_attributes => { :id => '2', :icon => 'sad' } } } + # params = { member: { avatar_attributes: { id: '2', icon: 'sad' } } } # member.update_attributes params[:member] # member.avatar.icon # => 'sad' # @@ -72,13 +68,13 @@ module ActiveRecord # # class Member < ActiveRecord::Base # has_one :avatar - # accepts_nested_attributes_for :avatar, :allow_destroy => true + # accepts_nested_attributes_for :avatar, allow_destroy: true # end # # Now, when you add the <tt>_destroy</tt> key to the attributes hash, with a # value that evaluates to +true+, you will destroy the associated model: # - # member.avatar_attributes = { :id => '2', :_destroy => '1' } + # member.avatar_attributes = { id: '2', _destroy: '1' } # member.avatar.marked_for_destruction? # => true # member.save # member.reload.avatar # => nil @@ -101,15 +97,15 @@ module ActiveRecord # be instantiated, unless the hash also contains a <tt>_destroy</tt> key # that evaluates to +true+. # - # params = { :member => { - # :name => 'joe', :posts_attributes => [ - # { :title => 'Kari, the awesome Ruby documentation browser!' }, - # { :title => 'The egalitarian assumption of the modern citizen' }, - # { :title => '', :_destroy => '1' } # this will be ignored + # params = { member: { + # name: 'joe', posts_attributes: [ + # { title: 'Kari, the awesome Ruby documentation browser!' }, + # { title: 'The egalitarian assumption of the modern citizen' }, + # { title: '', _destroy: '1' } # this will be ignored # ] # }} # - # member = Member.create(params['member']) + # member = Member.create(params[:member]) # member.posts.length # => 2 # member.posts.first.title # => 'Kari, the awesome Ruby documentation browser!' # member.posts.second.title # => 'The egalitarian assumption of the modern citizen' @@ -120,18 +116,18 @@ module ActiveRecord # # class Member < ActiveRecord::Base # has_many :posts - # accepts_nested_attributes_for :posts, :reject_if => proc { |attributes| attributes['title'].blank? } + # accepts_nested_attributes_for :posts, reject_if: proc { |attributes| attributes['title'].blank? } # end # - # params = { :member => { - # :name => 'joe', :posts_attributes => [ - # { :title => 'Kari, the awesome Ruby documentation browser!' }, - # { :title => 'The egalitarian assumption of the modern citizen' }, - # { :title => '' } # this will be ignored because of the :reject_if proc + # params = { member: { + # name: 'joe', posts_attributes: [ + # { title: 'Kari, the awesome Ruby documentation browser!' }, + # { title: 'The egalitarian assumption of the modern citizen' }, + # { title: '' } # this will be ignored because of the :reject_if proc # ] # }} # - # member = Member.create(params['member']) + # member = Member.create(params[:member]) # member.posts.length # => 2 # member.posts.first.title # => 'Kari, the awesome Ruby documentation browser!' # member.posts.second.title # => 'The egalitarian assumption of the modern citizen' @@ -140,12 +136,12 @@ module ActiveRecord # # class Member < ActiveRecord::Base # has_many :posts - # accepts_nested_attributes_for :posts, :reject_if => :new_record? + # accepts_nested_attributes_for :posts, reject_if: :new_record? # end # # class Member < ActiveRecord::Base # has_many :posts - # accepts_nested_attributes_for :posts, :reject_if => :reject_posts + # accepts_nested_attributes_for :posts, reject_if: :reject_posts # # def reject_posts(attributed) # attributed['title'].blank? @@ -156,10 +152,10 @@ module ActiveRecord # associated record, the matching record will be modified: # # member.attributes = { - # :name => 'Joe', - # :posts_attributes => [ - # { :id => 1, :title => '[UPDATED] An, as of yet, undisclosed awesome Ruby documentation browser!' }, - # { :id => 2, :title => '[UPDATED] other post' } + # name: 'Joe', + # posts_attributes: [ + # { id: 1, title: '[UPDATED] An, as of yet, undisclosed awesome Ruby documentation browser!' }, + # { id: 2, title: '[UPDATED] other post' } # ] # } # @@ -174,14 +170,14 @@ module ActiveRecord # # class Member < ActiveRecord::Base # has_many :posts - # accepts_nested_attributes_for :posts, :allow_destroy => true + # accepts_nested_attributes_for :posts, allow_destroy: true # end # - # params = { :member => { - # :posts_attributes => [{ :id => '2', :_destroy => '1' }] + # params = { member: { + # posts_attributes: [{ id: '2', _destroy: '1' }] # }} # - # member.attributes = params['member'] + # member.attributes = params[:member] # member.posts.detect { |p| p.id == 2 }.marked_for_destruction? # => true # member.posts.length # => 2 # member.save @@ -201,12 +197,12 @@ module ActiveRecord # <tt>inverse_of</tt> as this example illustrates: # # class Member < ActiveRecord::Base - # has_many :posts, :inverse_of => :member + # has_many :posts, inverse_of: :member # accepts_nested_attributes_for :posts # end # # class Post < ActiveRecord::Base - # belongs_to :member, :inverse_of => :posts + # belongs_to :member, inverse_of: :posts # validates_presence_of :member # end module ClassMethods @@ -252,11 +248,11 @@ module ActiveRecord # # Examples: # # creates avatar_attributes= - # accepts_nested_attributes_for :avatar, :reject_if => proc { |attributes| attributes['name'].blank? } + # accepts_nested_attributes_for :avatar, reject_if: proc { |attributes| attributes['name'].blank? } # # creates avatar_attributes= - # accepts_nested_attributes_for :avatar, :reject_if => :all_blank + # accepts_nested_attributes_for :avatar, reject_if: :all_blank # # creates avatar_attributes= and posts_attributes= - # accepts_nested_attributes_for :avatar, :posts, :allow_destroy => true + # accepts_nested_attributes_for :avatar, :posts, allow_destroy: true def accepts_nested_attributes_for(*attr_names) options = { :allow_destroy => false, :update_only => false } options.update(attr_names.extract_options!) @@ -352,9 +348,9 @@ module ActiveRecord # For example: # # assign_nested_attributes_for_collection_association(:people, { - # '1' => { :id => '1', :name => 'Peter' }, - # '2' => { :name => 'John' }, - # '3' => { :id => '2', :_destroy => true } + # '1' => { id: '1', name: 'Peter' }, + # '2' => { name: 'John' }, + # '3' => { id: '2', _destroy: true } # }) # # Will update the name of the Person with ID 1, build a new associated @@ -364,9 +360,9 @@ module ActiveRecord # Also accepts an Array of attribute hashes: # # assign_nested_attributes_for_collection_association(:people, [ - # { :id => '1', :name => 'Peter' }, - # { :name => 'John' }, - # { :id => '2', :_destroy => true } + # { id: '1', name: 'Peter' }, + # { name: 'John' }, + # { id: '2', _destroy: true } # ]) def assign_nested_attributes_for_collection_association(association_name, attributes_collection) options = self.nested_attributes_options[association_name] diff --git a/activerecord/lib/active_record/null_relation.rb b/activerecord/lib/active_record/null_relation.rb index 4c1c91e3df..711fc8b883 100644 --- a/activerecord/lib/active_record/null_relation.rb +++ b/activerecord/lib/active_record/null_relation.rb @@ -46,7 +46,11 @@ module ActiveRecord {} end - def count + def count(*) + 0 + end + + def sum(*) 0 end diff --git a/activerecord/lib/active_record/persistence.rb b/activerecord/lib/active_record/persistence.rb index 2eaad1d469..eed49e17b1 100644 --- a/activerecord/lib/active_record/persistence.rb +++ b/activerecord/lib/active_record/persistence.rb @@ -15,18 +15,18 @@ module ActiveRecord # # ==== Examples # # Create a single new object - # User.create(:first_name => 'Jamie') + # User.create(first_name: 'Jamie') # # # Create an Array of new objects - # User.create([{ :first_name => 'Jamie' }, { :first_name => 'Jeremy' }]) + # User.create([{ first_name: 'Jamie' }, { first_name: 'Jeremy' }]) # # # Create a single object and pass it into a block to set other attributes. - # User.create(:first_name => 'Jamie') do |u| + # User.create(first_name: 'Jamie') do |u| # u.is_admin = false # end # # # Creating an Array of new objects using a block, where the block is executed for each object: - # User.create([{ :first_name => 'Jamie' }, { :first_name => 'Jeremy' }]) do |u| + # User.create([{ first_name: 'Jamie' }, { first_name: 'Jeremy' }]) do |u| # u.is_admin = false # end def create(attributes = nil, &block) @@ -64,7 +64,7 @@ module ActiveRecord # # By default, save always run validations. If any of them fail the action # is cancelled and +save+ returns +false+. However, if you supply - # :validate => false, validations are bypassed altogether. See + # validate: false, validations are bypassed altogether. See # ActiveRecord::Validations for more information. # # There's a series of callbacks associated with +save+. If any of the @@ -72,11 +72,9 @@ module ActiveRecord # +save+ returns +false+. See ActiveRecord::Callbacks for further # details. def save(*) - begin - create_or_update - rescue ActiveRecord::RecordInvalid - false - end + create_or_update + rescue ActiveRecord::RecordInvalid + false end # Saves the model. @@ -143,7 +141,7 @@ module ActiveRecord # inheritance structures where you want a subclass to appear as the # superclass. This can be used along with record identification in # Action Pack to allow, say, <tt>Client < Company</tt> to do something - # like render <tt>:partial => @client.becomes(Company)</tt> to render that + # like render <tt>partial: @client.becomes(Company)</tt> to render that # instance using the companies/company partial instead of clients/client. # # Note: The new instance will share a link to the same attributes as the original class. @@ -155,7 +153,18 @@ module ActiveRecord became.instance_variable_set("@new_record", new_record?) became.instance_variable_set("@destroyed", destroyed?) became.instance_variable_set("@errors", errors) - became.public_send("#{klass.inheritance_column}=", klass.name) unless self.class.descends_from_active_record? + became + end + + # 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. + # + # Note: The old instance's sti column value will be changed too, as both objects + # share the same set of attributes. + def becomes!(klass) + became = becomes(klass) + became.public_send("#{klass.inheritance_column}=", klass.sti_name) unless self.class.descends_from_active_record? became end @@ -171,7 +180,7 @@ module ActiveRecord name = name.to_s verify_readonly_attribute(name) send("#{name}=", value) - save(:validate => false) + save(validate: false) end # Updates the attributes of the model from the passed-in hash and saves the @@ -197,7 +206,7 @@ module ActiveRecord end end - # Updates a single attribute of an object, without calling save. + # Updates a single attribute of an object, without having to explicitly call save on that object. # # * Validation is skipped. # * Callbacks are skipped. @@ -209,7 +218,7 @@ module ActiveRecord update_columns(name => value) end - # Updates the attributes from the passed-in hash, without calling save. + # Updates the attributes from the passed-in hash, without having to explicitly call save on that object. # # * Validation is skipped. # * Callbacks are skipped. @@ -224,11 +233,13 @@ module ActiveRecord verify_readonly_attribute(key.to_s) end - attributes.each do |k,v| - raw_write_attribute(k,v) + updated_count = self.class.where(self.class.primary_key => id).update_all(attributes) + + attributes.each do |k, v| + raw_write_attribute(k, v) end - self.class.where(self.class.primary_key => id).update_all(attributes) == 1 + updated_count == 1 end # Initializes +attribute+ to zero if +nil+ and adds the value passed as +by+ (default is 1). @@ -284,7 +295,7 @@ module ActiveRecord # Reloads the attributes of this object from the database. # The optional options argument is passed to find when reloading so you - # may do e.g. record.reload(:lock => true) to reload the same record with + # may do e.g. record.reload(lock: true) to reload the same record with # an exclusive row lock. def reload(options = nil) clear_aggregation_cache @@ -315,11 +326,11 @@ module ActiveRecord # If used along with +belongs_to+ then +touch+ will invoke +touch+ method on associated object. # # class Brake < ActiveRecord::Base - # belongs_to :car, :touch => true + # belongs_to :car, touch: true # end # # class Car < ActiveRecord::Base - # belongs_to :corporation, :touch => true + # belongs_to :corporation, touch: true # end # # # triggers @brake.car.touch and @brake.car.corporation.touch @@ -377,16 +388,20 @@ module ActiveRecord # Returns the number of affected rows. def update(attribute_names = @attributes.keys) attributes_with_values = arel_attributes_with_values_for_update(attribute_names) - return 0 if attributes_with_values.empty? - klass = self.class - stmt = klass.unscoped.where(klass.arel_table[klass.primary_key].eq(id)).arel.compile_update(attributes_with_values) - klass.connection.update stmt + + if attributes_with_values.empty? + 0 + else + klass = self.class + stmt = klass.unscoped.where(klass.arel_table[klass.primary_key].eq(id)).arel.compile_update(attributes_with_values) + klass.connection.update stmt + end end # Creates a record with values matching those of the instance attributes # and returns its id. - def create - attributes_values = arel_attributes_with_values_for_create(!id.nil?) + def create(attribute_names = @attributes.keys) + attributes_values = arel_attributes_with_values_for_create(attribute_names) new_id = self.class.unscoped.insert attributes_values self.id ||= new_id if self.class.primary_key diff --git a/activerecord/lib/active_record/query_cache.rb b/activerecord/lib/active_record/query_cache.rb index 2bd8ecda20..38e18b32a4 100644 --- a/activerecord/lib/active_record/query_cache.rb +++ b/activerecord/lib/active_record/query_cache.rb @@ -5,19 +5,19 @@ module ActiveRecord module ClassMethods # Enable the query cache within the block if Active Record is configured. def cache(&block) - if ActiveRecord::Base.configurations.blank? - yield - else + if ActiveRecord::Base.connected? connection.cache(&block) + else + yield end end # Disable the query cache within the block if Active Record is configured. def uncached(&block) - if ActiveRecord::Base.configurations.blank? - yield - else + if ActiveRecord::Base.connected? connection.uncached(&block) + else + yield end end end diff --git a/activerecord/lib/active_record/querying.rb b/activerecord/lib/active_record/querying.rb index 13e09eda53..45f6a78428 100644 --- a/activerecord/lib/active_record/querying.rb +++ b/activerecord/lib/active_record/querying.rb @@ -3,6 +3,7 @@ module ActiveRecord module Querying delegate :find, :take, :take!, :first, :first!, :last, :last!, :exists?, :any?, :many?, :to => :all delegate :first_or_create, :first_or_create!, :first_or_initialize, :to => :all + delegate :find_or_create_by, :find_or_create_by!, :find_or_initialize_by, :to => :all delegate :find_by, :find_by!, :to => :all delegate :destroy, :destroy_all, :delete, :delete_all, :update, :update_all, :to => :all delegate :find_each, :find_in_batches, :to => :all diff --git a/activerecord/lib/active_record/railtie.rb b/activerecord/lib/active_record/railtie.rb index a9f80ccd5f..5464ca6066 100644 --- a/activerecord/lib/active_record/railtie.rb +++ b/activerecord/lib/active_record/railtie.rb @@ -10,7 +10,7 @@ require "action_controller/railtie" module ActiveRecord # = Active Record Railtie - class Railtie < Rails::Railtie + class Railtie < Rails::Railtie # :nodoc: config.active_record = ActiveSupport::OrderedOptions.new config.app_generators.orm :active_record, :migration => true, @@ -76,13 +76,13 @@ module ActiveRecord config.after_initialize do |app| ActiveSupport.on_load(:active_record) do filename = File.join(app.config.paths["db"].first, "schema_cache.dump") - + if File.file?(filename) cache = Marshal.load File.binread filename if cache.version == ActiveRecord::Migrator.current_version - ActiveRecord::Model.connection.schema_cache = cache + self.connection.schema_cache = cache else - warn "schema_cache.dump is expired. Current version is #{ActiveRecord::Migrator.current_version}, but cache version is #{cache.version}." + warn "Ignoring db/schema_cache.dump because it has expired. The current schema version is #{ActiveRecord::Migrator.current_version}, but the one in the cache is #{cache.version}." end end end @@ -92,6 +92,33 @@ module ActiveRecord initializer "active_record.set_configs" do |app| ActiveSupport.on_load(:active_record) do + begin + old_behavior, ActiveSupport::Deprecation.behavior = ActiveSupport::Deprecation.behavior, :stderr + whitelist_attributes = app.config.active_record.delete(:whitelist_attributes) + + if respond_to?(:mass_assignment_sanitizer=) + mass_assignment_sanitizer = nil + else + mass_assignment_sanitizer = app.config.active_record.delete(:mass_assignment_sanitizer) + end + + unless whitelist_attributes.nil? && mass_assignment_sanitizer.nil? + ActiveSupport::Deprecation.warn <<-EOF.strip_heredoc, [] + Model based mass assignment security has been extracted + out of Rails into a gem. Please use the new recommended protection model for + params or add `protected_attributes` to your Gemfile to use the old one. + + To disable this message remove the `whitelist_attributes` option from your + `config/application.rb` file and any `mass_assignment_sanitizer` options + from your `config/environments/*.rb` files. + + See http://edgeguides.rubyonrails.org/security.html#mass-assignment for more information + EOF + end + ensure + ActiveSupport::Deprecation.behavior = old_behavior + end + app.config.active_record.each do |k,v| send "#{k}=", v end @@ -122,25 +149,27 @@ module ActiveRecord ActiveSupport.on_load(:active_record) do ActionDispatch::Reloader.send(hook) do - ActiveRecord::Model.clear_reloadable_connections! - ActiveRecord::Model.clear_cache! + if ActiveRecord::Base.connected? + ActiveRecord::Base.clear_reloadable_connections! + ActiveRecord::Base.clear_cache! + end end end end initializer "active_record.add_watchable_files" do |app| - config.watchable_files.concat ["#{app.root}/db/schema.rb", "#{app.root}/db/structure.sql"] + path = app.paths["db"].first + config.watchable_files.concat ["#{path}/schema.rb", "#{path}/structure.sql"] end config.after_initialize do |app| ActiveSupport.on_load(:active_record) do - ActiveRecord::Model.instantiate_observers + instantiate_observers ActionDispatch::Reloader.to_prepare do - ActiveRecord::Model.instantiate_observers + ActiveRecord::Base.instantiate_observers end end - end end end diff --git a/activerecord/lib/active_record/railties/controller_runtime.rb b/activerecord/lib/active_record/railties/controller_runtime.rb index c5db9b4625..7695eacbff 100644 --- a/activerecord/lib/active_record/railties/controller_runtime.rb +++ b/activerecord/lib/active_record/railties/controller_runtime.rb @@ -2,7 +2,7 @@ require 'active_support/core_ext/module/attr_internal' require 'active_record/log_subscriber' module ActiveRecord - module Railties + module Railties # :nodoc: module ControllerRuntime #:nodoc: extend ActiveSupport::Concern @@ -37,7 +37,7 @@ module ActiveRecord end end - module ClassMethods + module ClassMethods # :nodoc: def log_process_action(payload) messages, db_runtime = super, payload[:db_runtime] messages << ("ActiveRecord: %.1fms" % db_runtime.to_f) if db_runtime diff --git a/activerecord/lib/active_record/railties/databases.rake b/activerecord/lib/active_record/railties/databases.rake index d134978128..0a9caa25b2 100644 --- a/activerecord/lib/active_record/railties/databases.rake +++ b/activerecord/lib/active_record/railties/databases.rake @@ -196,7 +196,7 @@ db_namespace = namespace :db do fixtures_dir = File.join [base_dir, ENV['FIXTURES_DIR']].compact (ENV['FIXTURES'] ? ENV['FIXTURES'].split(/,/) : Dir["#{fixtures_dir}/**/*.yml"].map {|f| f[(fixtures_dir.size + 1)..-5] }).each do |fixture_file| - ActiveRecord::Fixtures.create_fixtures(fixtures_dir, fixture_file) + ActiveRecord::FixtureSet.create_fixtures(fixtures_dir, fixture_file) end end @@ -207,13 +207,13 @@ db_namespace = namespace :db do label, id = ENV['LABEL'], ENV['ID'] raise 'LABEL or ID required' if label.blank? && id.blank? - puts %Q(The fixture ID for "#{label}" is #{ActiveRecord::Fixtures.identify(label)}.) if label + puts %Q(The fixture ID for "#{label}" is #{ActiveRecord::FixtureSet.identify(label)}.) if label base_dir = ENV['FIXTURES_PATH'] ? File.join(Rails.root, ENV['FIXTURES_PATH']) : File.join(Rails.root, 'test', 'fixtures') Dir["#{base_dir}/**/*.yml"].each do |file| if data = YAML::load(ERB.new(IO.read(file)).result) data.keys.each do |key| - key_id = ActiveRecord::Fixtures.identify(key) + key_id = ActiveRecord::FixtureSet.identify(key) if key == label || key_id == id.to_i puts "#{file}: #{key} (#{key_id})" @@ -307,7 +307,7 @@ db_namespace = namespace :db do # desc "Recreate the databases from the structure.sql file" task :load => [:environment, :load_config] do - current_config = ActiveRecord::Tasks::DatabaseTasks.current_config(:env => (ENV['RAILS_ENV'] || 'test')) + current_config = ActiveRecord::Tasks::DatabaseTasks.current_config filename = ENV['DB_STRUCTURE'] || File.join(Rails.root, "db", "structure.sql") case current_config['adapter'] when /mysql/, /postgresql/, /sqlite/ diff --git a/activerecord/lib/active_record/readonly_attributes.rb b/activerecord/lib/active_record/readonly_attributes.rb index b3c20c4aff..8499bb16e7 100644 --- a/activerecord/lib/active_record/readonly_attributes.rb +++ b/activerecord/lib/active_record/readonly_attributes.rb @@ -22,7 +22,8 @@ module ActiveRecord end def _attr_readonly - ActiveSupport::Deprecation.warn("Instance level _attr_readonly method is deprecated, please use class level method.") + message = "Instance level _attr_readonly method is deprecated, please use class level method." + ActiveSupport::Deprecation.warn message defined?(@_attr_readonly) ? @_attr_readonly : self.class._attr_readonly end end diff --git a/activerecord/lib/active_record/reflection.rb b/activerecord/lib/active_record/reflection.rb index f322b96f79..0103de4cbd 100644 --- a/activerecord/lib/active_record/reflection.rb +++ b/activerecord/lib/active_record/reflection.rb @@ -81,13 +81,13 @@ module ActiveRecord class MacroReflection # Returns the name of the macro. # - # <tt>composed_of :balance, :class_name => 'Money'</tt> returns <tt>:balance</tt> + # <tt>composed_of :balance, class_name: 'Money'</tt> returns <tt>:balance</tt> # <tt>has_many :clients</tt> returns <tt>:clients</tt> attr_reader :name # Returns the macro type. # - # <tt>composed_of :balance, :class_name => 'Money'</tt> returns <tt>:composed_of</tt> + # <tt>composed_of :balance, class_name: 'Money'</tt> returns <tt>:composed_of</tt> # <tt>has_many :clients</tt> returns <tt>:has_many</tt> attr_reader :macro @@ -95,7 +95,7 @@ module ActiveRecord # Returns the hash of options used for the macro. # - # <tt>composed_of :balance, :class_name => 'Money'</tt> returns <tt>{ :class_name => "Money" }</tt> + # <tt>composed_of :balance, class_name: 'Money'</tt> returns <tt>{ class_name: "Money" }</tt> # <tt>has_many :clients</tt> returns +{}+ attr_reader :options @@ -115,7 +115,7 @@ module ActiveRecord # Returns the class for the macro. # - # <tt>composed_of :balance, :class_name => 'Money'</tt> returns the Money class + # <tt>composed_of :balance, class_name: 'Money'</tt> returns the Money class # <tt>has_many :clients</tt> returns the Client class def klass @klass ||= class_name.constantize @@ -123,7 +123,7 @@ module ActiveRecord # Returns the class name for the macro. # - # <tt>composed_of :balance, :class_name => 'Money'</tt> returns <tt>'Money'</tt> + # <tt>composed_of :balance, class_name: 'Money'</tt> returns <tt>'Money'</tt> # <tt>has_many :clients</tt> returns <tt>'Client'</tt> def class_name @class_name ||= (options[:class_name] || derive_class_name).to_s @@ -315,10 +315,10 @@ module ActiveRecord # the parent's validation. # # Unless you explicitly disable validation with - # <tt>:validate => false</tt>, validation will take place when: + # <tt>validate: false</tt>, validation will take place when: # - # * you explicitly enable validation; <tt>:validate => true</tt> - # * you use autosave; <tt>:autosave => true</tt> + # * you explicitly enable validation; <tt>validate: true</tt> + # * you use autosave; <tt>autosave: true</tt> # * the association is a +has_many+ association def validate? !options[:validate].nil? ? options[:validate] : (options[:autosave] == true || macro == :has_many) @@ -399,7 +399,7 @@ module ActiveRecord # # class Post < ActiveRecord::Base # has_many :taggings - # has_many :tags, :through => :taggings + # has_many :tags, through: :taggings # end # def source_reflection @@ -411,7 +411,7 @@ module ActiveRecord # # class Post < ActiveRecord::Base # has_many :taggings - # has_many :tags, :through => :taggings + # has_many :tags, through: :taggings # end # # tags_reflection = Post.reflect_on_association(:tags) @@ -439,12 +439,12 @@ module ActiveRecord # # class Person # has_many :articles - # has_many :comment_tags, :through => :articles + # has_many :comment_tags, through: :articles # end # # class Article # has_many :comments - # has_many :comment_tags, :through => :comments, :source => :tags + # has_many :comment_tags, through: :comments, source: :tags # end # # class Comment diff --git a/activerecord/lib/active_record/relation.rb b/activerecord/lib/active_record/relation.rb index ed80422336..3ee55c580e 100644 --- a/activerecord/lib/active_record/relation.rb +++ b/activerecord/lib/active_record/relation.rb @@ -91,8 +91,10 @@ module ActiveRecord end def initialize_copy(other) - @values = @values.dup - @values[:bind] = @values[:bind].dup if @values[:bind] + # This method is a hot spot, so for now, use Hash[] to dup the hash. + # https://bugs.ruby-lang.org/issues/7166 + @values = Hash[@values] + @values[:bind] = @values[:bind].dup if @values.key? :bind reset end @@ -127,46 +129,53 @@ module ActiveRecord scoping { @klass.create!(*args, &block) } end - # Tries to load the first record; if it fails, then <tt>create</tt> is called with the same arguments as this method. - # - # Expects arguments in the same format as +Base.create+. + def first_or_create(attributes = nil, &block) # :nodoc: + first || create(attributes, &block) + end + + def first_or_create!(attributes = nil, &block) # :nodoc: + first || create!(attributes, &block) + end + + def first_or_initialize(attributes = nil, &block) # :nodoc: + first || new(attributes, &block) + end + + # Finds the first record with the given attributes, or creates a record with the attributes + # if one is not found. # # ==== Examples # # Find the first user named Penélope or create a new one. - # User.where(:first_name => 'Penélope').first_or_create + # User.find_or_create_by(first_name: 'Penélope') # # => <User id: 1, first_name: 'Penélope', last_name: nil> # # # Find the first user named Penélope or create a new one. # # We already have one so the existing record will be returned. - # User.where(:first_name => 'Penélope').first_or_create + # User.find_or_create_by(first_name: 'Penélope') # # => <User id: 1, first_name: 'Penélope', last_name: nil> # # # Find the first user named Scarlett or create a new one with a particular last name. - # User.where(:first_name => 'Scarlett').first_or_create(:last_name => 'Johansson') + # User.create_with(last_name: 'Johansson').find_or_create_by(first_name: 'Scarlett') # # => <User id: 2, first_name: 'Scarlett', last_name: 'Johansson'> # # # Find the first user named Scarlett or create a new one with a different last name. # # We already have one so the existing record will be returned. - # User.where(:first_name => 'Scarlett').first_or_create do |user| + # User.find_or_create_by(first_name: 'Scarlett') do |user| # user.last_name = "O'Hara" # end # # => <User id: 2, first_name: 'Scarlett', last_name: 'Johansson'> - def first_or_create(attributes = nil, &block) - first || create(attributes, &block) + def find_or_create_by(attributes, &block) + find_by(attributes) || create(attributes, &block) end - # Like <tt>first_or_create</tt> but calls <tt>create!</tt> so an exception is raised if the created record is invalid. - # - # Expects arguments in the same format as <tt>Base.create!</tt>. - def first_or_create!(attributes = nil, &block) - first || create!(attributes, &block) + # Like <tt>find_or_create_by</tt>, but calls <tt>create!</tt> 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>first_or_create</tt> but calls <tt>new</tt> instead of <tt>create</tt>. - # - # Expects arguments in the same format as <tt>Base.new</tt>. - def first_or_initialize(attributes = nil, &block) - first || new(attributes, &block) + # Like <tt>find_or_create_by</tt>, but calls <tt>new</tt> instead of <tt>create</tt>. + def find_or_initialize_by(attributes, &block) + find_by(attributes) || new(attributes, &block) end # Runs EXPLAIN on the query or queries triggered by this relation and @@ -226,7 +235,7 @@ module ActiveRecord # Scope all queries to the current scope. # - # Comment.where(:post_id => 1).scoping do + # Comment.where(post_id: 1).scoping do # Comment.first # SELECT * FROM comments WHERE post_id = 1 # end # @@ -257,7 +266,7 @@ module ActiveRecord # Book.where('title LIKE ?', '%Rails%').update_all(author: 'David') # # # Update all books that match conditions, but limit it to 5 ordered by date - # Book.where('title LIKE ?', '%Rails%').order(:created_at).limit(5).update_all(:author => 'David') + # Book.where('title LIKE ?', '%Rails%').order(:created_at).limit(5).update_all(author: 'David') def update_all(updates) raise ArgumentError, "Empty list of attributes to change" if updates.blank? @@ -330,7 +339,7 @@ module ActiveRecord # # Person.destroy_all("last_login < '2004-04-04'") # Person.destroy_all(status: "inactive") - # Person.where(:age => 0..18).destroy_all + # Person.where(age: 0..18).destroy_all def destroy_all(conditions = nil) if conditions where(conditions).destroy_all @@ -375,7 +384,7 @@ module ActiveRecord # # Post.delete_all("person_id = 5 AND (category = 'Something' OR category = 'Else')") # Post.delete_all(["person_id = ? AND (category = ? OR category = ?)", 5, 'Something', 'Else']) - # Post.where(:person_id => 5).where(:category => ['Something', 'Else']).delete_all + # Post.where(person_id: 5).where(category: ['Something', 'Else']).delete_all # # Both calls delete the affected posts all at once with a single DELETE statement. # If you need to destroy dependent associations or call your <tt>before_*</tt> or @@ -477,7 +486,7 @@ module ActiveRecord # Returns a hash of where conditions # # Users.where(name: 'Oscar').where_values_hash - # # => {:name=>"oscar"} + # # => {name: "oscar"} def where_values_hash equalities = with_default_scope.where_values.grep(Arel::Nodes::Equality).find_all { |node| node.left.relation.name == table_name @@ -505,7 +514,7 @@ module ActiveRecord # Joins that are also marked for preloading. In which case we should just eager load them. # Note that this is a naive implementation because we could have strings and symbols which # represent the same association, but that aren't matched by this. Also, we could have - # nested hashes which partially match, e.g. { :a => :b } & { :a => [:b, :c] } + # nested hashes which partially match, e.g. { a: :b } & { a: [:b, :c] } def joined_includes_values includes_values & joins_values end @@ -540,7 +549,7 @@ module ActiveRecord end def values - @values.dup + Hash[@values] end def inspect diff --git a/activerecord/lib/active_record/relation/batches.rb b/activerecord/lib/active_record/relation/batches.rb index d32048cce1..b921f2eddb 100644 --- a/activerecord/lib/active_record/relation/batches.rb +++ b/activerecord/lib/active_record/relation/batches.rb @@ -36,12 +36,12 @@ module ActiveRecord # want multiple workers dealing with the same processing queue. You can # make worker 1 handle all the records between id 0 and 10,000 and # worker 2 handle from 10,000 and beyond (by setting the +:start+ - # option on that worker). You can also use non-integer-based primary keys - # if start point is set. + # option on that worker). # # It's not possible to set the order. That is automatically set to - # ascending on the primary key (e.g. "id ASC") to make the batch ordering - # work. You can't set the limit either, that's used to control + # ascending on the primary key ("id ASC") to make the batch ordering + # work. This also means that this method only works with integer-based + # primary keys. You can't set the limit either, that's used to control # the batch sizes. # # Person.where("age > 21").find_in_batches do |group| @@ -63,15 +63,14 @@ module ActiveRecord end start = options.delete(:start) - start ||= 0 batch_size = options.delete(:batch_size) || 1000 relation = relation.reorder(batch_order).limit(batch_size) - records = relation.where(table[primary_key].gteq(start)).to_a + records = start ? relation.where(table[primary_key].gteq(start)).to_a : relation.to_a while records.any? records_size = records.size - primary_key_offset = records.last.send(primary_key) + primary_key_offset = records.last.id yield records diff --git a/activerecord/lib/active_record/relation/calculations.rb b/activerecord/lib/active_record/relation/calculations.rb index 7c43d844d0..741f94f777 100644 --- a/activerecord/lib/active_record/relation/calculations.rb +++ b/activerecord/lib/active_record/relation/calculations.rb @@ -1,5 +1,3 @@ -require 'active_support/core_ext/object/try' - module ActiveRecord module Calculations # Count the records. @@ -145,7 +143,7 @@ module ActiveRecord # # SELECT DISTINCT role FROM people # # => ['admin', 'member', 'guest'] # - # Person.where(:age => 21).limit(5).pluck(:id) + # Person.where(age: 21).limit(5).pluck(:id) # # SELECT people.id FROM people WHERE people.age = 21 LIMIT 5 # # => [2, 3] # @@ -165,7 +163,9 @@ module ActiveRecord if has_include?(column_names.first) construct_relation_for_association_calculations.pluck(*column_names) else - result = klass.connection.select_all(select(column_names).arel, nil, bind_values) + relation = spawn + relation.select_values = column_names + result = klass.connection.select_all(relation.arel, nil, bind_values) columns = result.columns.map do |key| klass.column_types.fetch(key) { result.column_types.fetch(key) { @@ -271,17 +271,19 @@ module ActiveRecord group_fields = group_attrs end - group_aliases = group_fields.map { |field| column_alias_for(field) } + group_aliases = group_fields.map { |field| + column_alias_for(field) + } group_columns = group_aliases.zip(group_fields).map { |aliaz,field| [aliaz, column_for(field)] } - group = @klass.connection.adapter_name == 'FrontBase' ? group_aliases : group_fields + group = group_fields if operation == 'count' && column_name == :all aggregate_alias = 'count_all' else - aggregate_alias = column_alias_for(operation, column_name) + aggregate_alias = column_alias_for([operation, column_name].join(' ')) end select_values = [ @@ -300,7 +302,8 @@ module ActiveRecord end } - relation = except(:group).group(group) + relation = except(:group) + relation.group_values = group relation.select_values = select_values calculated_data = @klass.connection.select_all(relation, nil, bind_values) @@ -329,10 +332,12 @@ module ActiveRecord # column_alias_for("count(distinct users.id)") # => "count_distinct_users_id" # column_alias_for("count(*)") # => "count_all" # column_alias_for("count", "id") # => "count_id" - def column_alias_for(*keys) - keys.map! {|k| k.respond_to?(:to_sql) ? k.to_sql : k} - table_name = keys.join(' ') - table_name.downcase! + def column_alias_for(keys) + if keys.respond_to? :name + keys = "#{keys.relation.name}.#{keys.name}" + end + + table_name = keys.to_s.downcase table_name.gsub!(/\*/, 'all') table_name.gsub!(/\W+/, ' ') table_name.strip! @@ -343,13 +348,13 @@ module ActiveRecord def column_for(field) field_name = field.respond_to?(:name) ? field.name.to_s : field.to_s.split('.').last - @klass.columns.detect { |c| c.name.to_s == field_name } + @klass.columns_hash[field_name] end def type_cast_calculated_value(value, column, operation = nil) case operation when 'count' then value.to_i - when 'sum' then type_cast_using_column(value || '0', column) + when 'sum' then type_cast_using_column(value || 0, column) when 'average' then value.respond_to?(:to_d) ? value.to_d : value else type_cast_using_column(value, column) end diff --git a/activerecord/lib/active_record/relation/delegation.rb b/activerecord/lib/active_record/relation/delegation.rb index ab8b36c8ab..dbfa92bbbd 100644 --- a/activerecord/lib/active_record/relation/delegation.rb +++ b/activerecord/lib/active_record/relation/delegation.rb @@ -1,3 +1,4 @@ +require 'thread' module ActiveRecord module Delegation # :nodoc: @@ -6,6 +7,8 @@ module ActiveRecord delegate :table_name, :quoted_table_name, :primary_key, :quoted_primary_key, :connection, :columns_hash, :auto_explain_threshold_in_seconds, :to => :klass + @@delegation_mutex = Mutex.new + def self.delegate_to_scoped_klass(method) if method.to_s =~ /\A[a-zA-Z_]\w*[!?]?\z/ module_eval <<-RUBY, __FILE__, __LINE__ + 1 @@ -32,13 +35,28 @@ module ActiveRecord def method_missing(method, *args, &block) if @klass.respond_to?(method) - ::ActiveRecord::Delegation.delegate_to_scoped_klass(method) + @@delegation_mutex.synchronize do + unless ::ActiveRecord::Delegation.method_defined?(method) + ::ActiveRecord::Delegation.delegate_to_scoped_klass(method) + end + end + scoping { @klass.send(method, *args, &block) } elsif Array.method_defined?(method) - ::ActiveRecord::Delegation.delegate method, :to => :to_a + @@delegation_mutex.synchronize do + unless ::ActiveRecord::Delegation.method_defined?(method) + ::ActiveRecord::Delegation.delegate method, :to => :to_a + end + end + to_a.send(method, *args, &block) elsif arel.respond_to?(method) - ::ActiveRecord::Delegation.delegate method, :to => :arel + @@delegation_mutex.synchronize do + unless ::ActiveRecord::Delegation.method_defined?(method) + ::ActiveRecord::Delegation.delegate method, :to => :arel + end + end + arel.send(method, *args, &block) else super diff --git a/activerecord/lib/active_record/relation/finder_methods.rb b/activerecord/lib/active_record/relation/finder_methods.rb index 84aaa39fed..eafe4a54c4 100644 --- a/activerecord/lib/active_record/relation/finder_methods.rb +++ b/activerecord/lib/active_record/relation/finder_methods.rb @@ -1,5 +1,3 @@ -require 'active_support/core_ext/hash/indifferent_access' - module ActiveRecord module FinderMethods # Find by id - This can either be a specific id (1), a list of ids (1, 5, 6), or an array of ids ([5, 6, 10]). @@ -78,7 +76,7 @@ module ActiveRecord # # Person.first # returns the first object fetched by SELECT * FROM people # Person.where(["user_name = ?", user_name]).first - # Person.where(["user_name = :u", { :u => user_name }]).first + # Person.where(["user_name = :u", { u: user_name }]).first # Person.order("created_on DESC").offset(5).first # Person.first(3) # returns the first three objects fetched by SELECT * FROM people LIMIT 3 def first(limit = nil) @@ -159,7 +157,7 @@ module ActiveRecord # Person.exists?(false) # Person.exists? def exists?(conditions = :none) - conditions = conditions.id if ActiveRecord::Model === conditions + conditions = conditions.id if Base === conditions return false if !conditions join_dependency = construct_join_dependency_for_association_find @@ -225,7 +223,7 @@ module ActiveRecord def construct_limited_ids_condition(relation) orders = relation.order_values.map { |val| val.presence }.compact - values = @klass.connection.distinct("#{@klass.connection.quote_table_name table_name}.#{primary_key}", orders) + values = @klass.connection.distinct("#{quoted_table_name}.#{primary_key}", orders) relation = relation.dup @@ -234,8 +232,6 @@ module ActiveRecord end def find_with_ids(*ids) - return to_a.find { |*block_args| yield(*block_args) } if block_given? - expects_array = ids.first.kind_of?(Array) return ids.first if expects_array && ids.first.empty? diff --git a/activerecord/lib/active_record/relation/merger.rb b/activerecord/lib/active_record/relation/merger.rb index e5b50673da..59226d316e 100644 --- a/activerecord/lib/active_record/relation/merger.rb +++ b/activerecord/lib/active_record/relation/merger.rb @@ -22,7 +22,17 @@ module ActiveRecord # the values. def other other = Relation.new(relation.klass, relation.table) - hash.each { |k, v| other.send("#{k}!", v) } + hash.each { |k, v| + if k == :joins + if Hash === v + other.joins!(v) + else + other.joins!(*v) + end + else + other.send("#{k}!", v) + end + } other end end @@ -39,16 +49,18 @@ module ActiveRecord @values = other.values end + NORMAL_VALUES = Relation::SINGLE_VALUE_METHODS + + Relation::MULTI_VALUE_METHODS - + [:where, :order, :bind, :reverse_order, :lock, :create_with, :reordering, :from] # :nodoc: + def normal_values - Relation::SINGLE_VALUE_METHODS + - Relation::MULTI_VALUE_METHODS - - [:where, :order, :bind, :reverse_order, :lock, :create_with, :reordering, :from] + NORMAL_VALUES end def merge normal_values.each do |name| value = values[name] - relation.send("#{name}!", value) unless value.blank? + relation.send("#{name}!", *value) unless value.blank? end merge_multi_values diff --git a/activerecord/lib/active_record/relation/predicate_builder.rb b/activerecord/lib/active_record/relation/predicate_builder.rb index 263fdce250..83074e72c1 100644 --- a/activerecord/lib/active_record/relation/predicate_builder.rb +++ b/activerecord/lib/active_record/relation/predicate_builder.rb @@ -36,11 +36,11 @@ module ActiveRecord queries = [] # Find the foreign key when using queries such as: - # Post.where(:author => author) + # Post.where(author: author) # # For polymorphic relationships, find the foreign key and type: - # PriceEstimate.where(:estimate_of => treasure) - if klass && value.class < Model::Tag && reflection = klass.reflect_on_association(column.to_sym) + # PriceEstimate.where(estimate_of: treasure) + if klass && value.class < Base && reflection = klass.reflect_on_association(column.to_sym) if reflection.polymorphic? queries << build(table[reflection.foreign_type], value.class.base_class) end @@ -67,7 +67,7 @@ module ActiveRecord def self.build(attribute, value) case value when Array, ActiveRecord::Associations::CollectionProxy - values = value.to_a.map {|x| x.is_a?(ActiveRecord::Model) ? x.id : x} + values = value.to_a.map {|x| x.is_a?(Base) ? x.id : x} ranges, values = values.partition {|v| v.is_a?(Range)} values_predicate = if values.include?(nil) @@ -93,7 +93,7 @@ module ActiveRecord attribute.in(value.arel.ast) when Range attribute.in(value) - when ActiveRecord::Model + when ActiveRecord::Base attribute.eq(value.id) when Class # FIXME: I think we need to deprecate this behavior diff --git a/activerecord/lib/active_record/relation/query_methods.rb b/activerecord/lib/active_record/relation/query_methods.rb index 3c59bd8a68..b3712b4ad6 100644 --- a/activerecord/lib/active_record/relation/query_methods.rb +++ b/activerecord/lib/active_record/relation/query_methods.rb @@ -202,6 +202,15 @@ module ActiveRecord # # User.order('name DESC, email') # => SELECT "users".* FROM "users" ORDER BY name DESC, email + # + # User.order(:name) + # => SELECT "users".* FROM "users" ORDER BY "users"."name" ASC + # + # User.order(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 def order(*args) args.blank? ? self : spawn.order!(*args) end @@ -209,6 +218,7 @@ module ActiveRecord # Like #order, but modifies relation in place. def order!(*args) args.flatten! + validate_order_args args references = args.reject { |arg| Arel::Node === arg } references.map! { |arg| arg =~ /^([a-zA-Z]\w*)\.(\w+)/ && $1 }.compact! @@ -234,6 +244,7 @@ module ActiveRecord # Like #reorder, but modifies relation in place. def reorder!(*args) args.flatten! + validate_order_args args self.reordering_value = true self.order_values = args @@ -245,13 +256,11 @@ module ActiveRecord # User.joins(:posts) # => SELECT "users".* FROM "users" INNER JOIN "posts" ON "posts"."user_id" = "users"."id" def joins(*args) - args.compact.blank? ? self : spawn.joins!(*args) + args.compact.blank? ? self : spawn.joins!(*args.flatten) end # Like #joins, but modifies relation in place. def joins!(*args) - args.flatten! - self.joins_values += args self end @@ -346,17 +355,17 @@ module ActiveRecord # author = Author.find(1) # # # The following queries will be equivalent: - # Post.where(:author => author) - # Post.where(:author_id => author) + # Post.where(author: author) + # Post.where(author_id: author) # # This also works with polymorphic belongs_to relationships: # - # treasure = Treasure.create(:name => 'gold coins') - # treasure.price_estimates << PriceEstimate.create(:price => 125) + # treasure = Treasure.create(name: 'gold coins') + # treasure.price_estimates << PriceEstimate.create(price: 125) # # # The following queries will be equivalent: - # PriceEstimate.where(:estimate_of => treasure) - # PriceEstimate.where(:estimate_of_type => 'Treasure', :estimate_of_id => treasure) + # PriceEstimate.where(estimate_of: treasure) + # PriceEstimate.where(estimate_of_type: 'Treasure', estimate_of_id: treasure) # # === Joins # @@ -368,7 +377,7 @@ module ActiveRecord # For hash conditions, you can either use the table name in the key, or use a sub-hash. # # User.joins(:posts).where({ "posts.published" => true }) - # User.joins(:posts).where({ :posts => { :published => true } }) + # User.joins(:posts).where({ posts: { published: true } }) # # === empty condition # @@ -467,13 +476,13 @@ module ActiveRecord # # For example: # - # @posts = current_user.visible_posts.where(:name => params[:name]) + # @posts = current_user.visible_posts.where(name: params[:name]) # # => the visible_posts method is expected to return a chainable Relation # # def visible_posts # case role # when 'Country Manager' - # Post.where(:country => country) + # Post.where(country: country) # when 'Reviewer' # Post.published # when 'Bad User' @@ -485,6 +494,11 @@ module ActiveRecord extending(NullRelation) end + # Like #none, but modifies relation in place. + def none! + extending!(NullRelation) + end + # Sets readonly attributes for the returned relation. If value is # true (default), attempting to update a record will result in an error. # @@ -658,9 +672,7 @@ module ActiveRecord arel.group(*group_values.uniq.reject{|g| g.blank?}) unless group_values.empty? - order = order_values - order = reverse_sql_order(order) if reverse_order_value - arel.order(*order.uniq.reject{|o| o.blank?}) unless order.empty? + build_order(arel) build_select(arel, select_values.uniq) @@ -729,22 +741,22 @@ module ActiveRecord buckets = joins.group_by do |join| case join when String - 'string_join' + :string_join when Hash, Symbol, Array - 'association_join' + :association_join when ActiveRecord::Associations::JoinDependency::JoinAssociation - 'stashed_join' + :stashed_join when Arel::Nodes::Join - 'join_node' + :join_node else raise 'unknown class: %s' % join.class.name end end - association_joins = buckets['association_join'] || [] - stashed_association_joins = buckets['stashed_join'] || [] - join_nodes = (buckets['join_node'] || []).uniq - string_joins = (buckets['string_join'] || []).map { |x| + association_joins = buckets[:association_join] || [] + stashed_association_joins = buckets[:stashed_join] || [] + join_nodes = (buckets[:join_node] || []).uniq + string_joins = (buckets[:string_join] || []).map { |x| x.strip }.uniq @@ -782,24 +794,56 @@ module ActiveRecord def reverse_sql_order(order_query) order_query = ["#{quoted_table_name}.#{quoted_primary_key} ASC"] if order_query.empty? - order_query.map do |o| + order_query.flat_map do |o| case o when Arel::Nodes::Ordering o.reverse - when String, Symbol + when String o.to_s.split(',').collect do |s| s.strip! s.gsub!(/\sasc\Z/i, ' DESC') || s.gsub!(/\sdesc\Z/i, ' ASC') || s.concat(' DESC') end + when Symbol + { o => :desc } + when Hash + o.each_with_object({}) do |(field, dir), memo| + memo[field] = (dir == :asc ? :desc : :asc ) + end else o end - end.flatten + end end def array_of_strings?(o) o.is_a?(Array) && o.all?{|obj| obj.is_a?(String)} end + def build_order(arel) + orders = order_values + orders = reverse_sql_order(orders) if reverse_order_value + + orders = orders.uniq.reject(&:blank?).flat_map do |order| + case order + when Symbol + table[order].asc + when Hash + order.map { |field, dir| table[field].send(dir) } + else + order + end + end + + arel.order(*orders) unless orders.empty? + end + + def validate_order_args(args) + args.select { |a| Hash === a }.each do |h| + unless (h.values - [:asc, :desc]).empty? + raise ArgumentError, 'Direction should be :asc or :desc' + end + end + end + end end diff --git a/activerecord/lib/active_record/relation/spawn_methods.rb b/activerecord/lib/active_record/relation/spawn_methods.rb index 5394c1b28b..62dda542ab 100644 --- a/activerecord/lib/active_record/relation/spawn_methods.rb +++ b/activerecord/lib/active_record/relation/spawn_methods.rb @@ -15,11 +15,11 @@ module ActiveRecord # # ==== Examples # - # Post.where(:published => true).joins(:comments).merge( Comment.where(:spam => false) ) + # Post.where(published: true).joins(:comments).merge( Comment.where(spam: false) ) # # Performs a single join query with both where conditions. # # recent_posts = Post.order('created_at DESC').first(5) - # Post.where(:published => true).merge(recent_posts) + # Post.where(published: true).merge(recent_posts) # # Returns the intersection of all published posts with the 5 most recently created posts. # # (This is just an example. You'd probably want to do this with a single query!) # diff --git a/activerecord/lib/active_record/result.rb b/activerecord/lib/active_record/result.rb index 8905137142..bea195e9b8 100644 --- a/activerecord/lib/active_record/result.rb +++ b/activerecord/lib/active_record/result.rb @@ -53,12 +53,15 @@ module ActiveRecord private def hash_rows - @hash_rows ||= @rows.map { |row| - # We freeze the strings to prevent them getting duped when - # used as keys in ActiveRecord::Model's @attributes hash - columns = @columns.map { |c| c.freeze } - Hash[columns.zip(row)] - } + @hash_rows ||= + begin + # We freeze the strings to prevent them getting duped when + # used as keys in ActiveRecord::Base's @attributes hash + columns = @columns.map { |c| c.dup.freeze } + @rows.map { |row| + Hash[columns.zip(row)] + } + end end end end diff --git a/activerecord/lib/active_record/sanitization.rb b/activerecord/lib/active_record/sanitization.rb index 42b4cff4b8..2dad1dc177 100644 --- a/activerecord/lib/active_record/sanitization.rb +++ b/activerecord/lib/active_record/sanitization.rb @@ -17,7 +17,7 @@ module ActiveRecord # Accepts an array, hash, or string of SQL conditions and sanitizes # them into a valid SQL fragment for a WHERE clause. # ["name='%s' and group_id='%s'", "foo'bar", 4] returns "name='foo''bar' and group_id='4'" - # { :name => "foo'bar", :group_id => 4 } returns "name='foo''bar' and group_id='4'" + # { name: "foo'bar", group_id: 4 } returns "name='foo''bar' and group_id='4'" # "name='foo''bar' and group_id='4'" returns "name='foo''bar' and group_id='4'" def sanitize_sql_for_conditions(condition, table_name = self.table_name) return nil if condition.blank? @@ -32,7 +32,7 @@ module ActiveRecord # Accepts an array, hash, or string of SQL conditions and sanitizes # them into a valid SQL fragment for a SET clause. - # { :name => nil, :group_id => 4 } returns "name = NULL , group_id='4'" + # { name: nil, group_id: 4 } returns "name = NULL , group_id='4'" def sanitize_sql_for_assignment(assignments) case assignments when Array; sanitize_sql_array(assignments) @@ -46,12 +46,12 @@ module ActiveRecord # aggregate attribute values. # Given: # class Person < ActiveRecord::Base - # composed_of :address, :class_name => "Address", - # :mapping => [%w(address_street street), %w(address_city city)] + # composed_of :address, class_name: "Address", + # mapping: [%w(address_street street), %w(address_city city)] # end # Then: - # { :address => Address.new("813 abc st.", "chicago") } - # # => { :address_street => "813 abc st.", :address_city => "chicago" } + # { address: Address.new("813 abc st.", "chicago") } + # # => { address_street: "813 abc st.", address_city: "chicago" } def expand_hash_conditions_for_aggregates(attrs) expanded_attrs = {} attrs.each do |attr, value| @@ -72,18 +72,18 @@ module ActiveRecord end # Sanitizes a hash of attribute/value pairs into SQL conditions for a WHERE clause. - # { :name => "foo'bar", :group_id => 4 } + # { name: "foo'bar", group_id: 4 } # # => "name='foo''bar' and group_id= 4" - # { :status => nil, :group_id => [1,2,3] } + # { status: nil, group_id: [1,2,3] } # # => "status IS NULL and group_id IN (1,2,3)" - # { :age => 13..18 } + # { age: 13..18 } # # => "age BETWEEN 13 AND 18" # { 'other_records.id' => 7 } # # => "`other_records`.`id` = 7" - # { :other_records => { :id => 7 } } + # { other_records: { id: 7 } } # # => "`other_records`.`id` = 7" # And for value objects on a composed_of relationship: - # { :address => Address.new("123 abc st.", "chicago") } + # { address: Address.new("123 abc st.", "chicago") } # # => "address_street='123 abc st.' and address_city='chicago'" def sanitize_sql_hash_for_conditions(attrs, default_table_name = self.table_name) attrs = expand_hash_conditions_for_aggregates(attrs) @@ -96,7 +96,7 @@ module ActiveRecord alias_method :sanitize_sql_hash, :sanitize_sql_hash_for_conditions # Sanitizes a hash of attribute/value pairs into SQL conditions for a SET clause. - # { :status => nil, :group_id => 1 } + # { status: nil, group_id: 1 } # # => "status = NULL , group_id = 1" def sanitize_sql_hash_for_assignment(attrs) attrs.map do |attr, value| @@ -141,23 +141,6 @@ module ActiveRecord end end - def expand_range_bind_variables(bind_vars) #:nodoc: - expanded = [] - - bind_vars.each do |var| - next if var.is_a?(Hash) - - if var.is_a?(Range) - expanded << var.first - expanded << var.last - else - expanded << var - end - end - - expanded - end - def quote_bound_value(value, c = connection) #:nodoc: if value.respond_to?(:map) && !value.acts_like?(:string) if value.respond_to?(:empty?) && value.empty? diff --git a/activerecord/lib/active_record/scoping/default.rb b/activerecord/lib/active_record/scoping/default.rb index a2a85d4b96..6835d0e01b 100644 --- a/activerecord/lib/active_record/scoping/default.rb +++ b/activerecord/lib/active_record/scoping/default.rb @@ -5,17 +5,17 @@ module ActiveRecord extend ActiveSupport::Concern included do - # Stores the default scope for the class + # Stores the default scope for the class. class_attribute :default_scopes, instance_writer: false self.default_scopes = [] end module ClassMethods - # Returns a scope for the model without the default_scope. + # Returns a scope for the model without the +default_scope+. # # class Post < ActiveRecord::Base # def self.default_scope - # where :published => true + # where published: true # end # end # @@ -23,16 +23,16 @@ module ActiveRecord # Post.unscoped.all # Fires "SELECT * FROM posts" # # This method also accepts a block. All queries inside the block will - # not use the default_scope: + # not use the +default_scope+: # # Post.unscoped { # Post.limit(10) # Fires "SELECT * FROM posts LIMIT 10" # } # # It is recommended that you use the block form of unscoped because - # chaining unscoped with <tt>scope</tt> does not work. Assuming that - # <tt>published</tt> is a <tt>scope</tt>, the following two statements - # are equal: the <tt>default_scope</tt> is applied on both. + # chaining unscoped with +scope+ does not work. Assuming that + # +published+ is a +scope+, the following two statements + # are equal: the +default_scope+ is applied on both. # # Post.unscoped.published # Post.published @@ -50,35 +50,37 @@ module ActiveRecord # the model. # # class Article < ActiveRecord::Base - # default_scope { where(:published => true) } + # default_scope { where(published: true) } # end # # Article.all # => SELECT * FROM articles WHERE published = true # - # The <tt>default_scope</tt> is also applied while creating/building a record. It is not - # applied while updating 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 # Article.create.published # => true # - # (You can also pass any object which responds to <tt>call</tt> to the <tt>default_scope</tt> - # macro, and it will be called when building the default scope.) + # (You can also pass any object which responds to +call+ to the + # +default_scope+ macro, and it will be called when building the + # default scope.) # - # If you use multiple <tt>default_scope</tt> declarations in your model then they will - # be merged together: + # If you use multiple +default_scope+ declarations in your model then + # they will be merged together: # # class Article < ActiveRecord::Base - # default_scope { where(:published => true) } - # default_scope { where(:rating => 'G') } + # default_scope { where(published: true) } + # default_scope { where(rating: 'G') } # end # # Article.all # => SELECT * FROM articles WHERE published = true AND rating = 'G' # - # This is also the case with inheritance and module includes where the parent or module - # defines a <tt>default_scope</tt> and the child or including class defines a second one. + # This is also the case with inheritance and module includes where the + # 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 alternatively - # define it as a class method: + # If you need to do more complex things with a default scope, you can + # alternatively define it as a class method: # # class Article < ActiveRecord::Base # def self.default_scope @@ -100,7 +102,7 @@ module ActiveRecord self.default_scopes = default_scopes + [scope] end - def build_default_scope #:nodoc: + def build_default_scope # :nodoc: if !Base.is_a?(method(:default_scope).owner) # The user has defined their own default scope method, so call that evaluate_default_scope { default_scope } @@ -117,17 +119,18 @@ module ActiveRecord end end - def ignore_default_scope? #:nodoc: + def ignore_default_scope? # :nodoc: Thread.current["#{self}_ignore_default_scope"] end - def ignore_default_scope=(ignore) #:nodoc: + def ignore_default_scope=(ignore) # :nodoc: Thread.current["#{self}_ignore_default_scope"] = ignore end - # The ignore_default_scope flag is used to prevent an infinite recursion situation where - # a default scope references a scope which has a default scope which references a scope... - def evaluate_default_scope + # The ignore_default_scope flag is used to prevent an infinite recursion + # situation where a default scope references a scope which has a default + # scope which references a scope... + def evaluate_default_scope # :nodoc: return if ignore_default_scope? begin diff --git a/activerecord/lib/active_record/scoping/named.rb b/activerecord/lib/active_record/scoping/named.rb index 75f31229b5..fb5f5b5be0 100644 --- a/activerecord/lib/active_record/scoping/named.rb +++ b/activerecord/lib/active_record/scoping/named.rb @@ -3,7 +3,7 @@ require 'active_support/core_ext/hash/except' require 'active_support/core_ext/kernel/singleton_class' module ActiveRecord - # = Active Record Named \Scopes + # = Active Record \Named \Scopes module Scoping module Named extend ActiveSupport::Concern @@ -16,11 +16,11 @@ module ActiveRecord # posts.each {|p| puts p.name } # Fires "select * from posts" and loads post objects # # fruits = Fruit.all - # fruits = fruits.where(:color => 'red') if options[:red_only] + # fruits = fruits.where(color: 'red') if options[:red_only] # fruits = fruits.limit(10) if limited? # - # You can define a \scope that applies to all finders using - # ActiveRecord::Base.default_scope. + # You can define a scope that applies to all finders using + # <tt>ActiveRecord::Base.default_scope</tt>. def all if current_scope current_scope.clone @@ -31,7 +31,6 @@ module ActiveRecord end end - ## # Collects attributes from scopes that should be applied when creating # an AR instance for the particular class this is called on. def scope_attributes # :nodoc: @@ -44,86 +43,70 @@ module ActiveRecord end end - ## # Are there default attributes associated with this scope? def scope_attributes? # :nodoc: current_scope || default_scopes.any? end - # Adds a class method for retrieving and querying objects. A \scope represents a narrowing of a database query, - # such as <tt>where(:color => :red).select('shirts.*').includes(:washing_instructions)</tt>. + # Adds a class method for retrieving and querying objects. A \scope + # represents a narrowing of a database query, such as + # <tt>where(color: :red).select('shirts.*').includes(:washing_instructions)</tt>. # # class Shirt < ActiveRecord::Base - # scope :red, where(:color => 'red') - # scope :dry_clean_only, joins(:washing_instructions).where('washing_instructions.dry_clean_only = ?', true) + # scope :red, -> { where(color: 'red') } + # scope :dry_clean_only, -> { joins(:washing_instructions).where('washing_instructions.dry_clean_only = ?', true) } # end # - # The above calls to <tt>scope</tt> define class methods Shirt.red and Shirt.dry_clean_only. Shirt.red, - # in effect, represents the query <tt>Shirt.where(:color => 'red')</tt>. + # 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>. # - # Note that this is simply 'syntactic sugar' for defining an actual class method: + # You should always pass a callable object to the scopes defined + # 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 + # class method: # # class Shirt < ActiveRecord::Base # def self.red - # where(:color => 'red') + # where(color: 'red') # end # end # - # Unlike <tt>Shirt.find(...)</tt>, however, the object returned by Shirt.red is not an Array; it - # resembles the association object constructed by a <tt>has_many</tt> declaration. For instance, - # you can invoke <tt>Shirt.red.first</tt>, <tt>Shirt.red.count</tt>, <tt>Shirt.red.where(:size => 'small')</tt>. - # Also, just as with the association objects, named \scopes act like an Array, implementing Enumerable; - # <tt>Shirt.red.each(&block)</tt>, <tt>Shirt.red.first</tt>, and <tt>Shirt.red.inject(memo, &block)</tt> - # all behave as if Shirt.red really was an Array. - # - # These named \scopes are composable. For instance, <tt>Shirt.red.dry_clean_only</tt> will produce - # all shirts that are both red and dry clean only. - # Nested finds and calculations also work with these compositions: <tt>Shirt.red.dry_clean_only.count</tt> - # returns the number of garments for which these criteria obtain. Similarly with - # <tt>Shirt.red.dry_clean_only.average(:thread_count)</tt>. - # - # All \scopes are available as class methods on the ActiveRecord::Base descendant upon which - # the \scopes were defined. But they are also available to <tt>has_many</tt> associations. If, + # 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.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. + # + # These named \scopes are composable. For instance, + # <tt>Shirt.red.dry_clean_only</tt> will produce all shirts that are + # both red and dry clean only. Nested finds and calculations also work + # with these compositions: <tt>Shirt.red.dry_clean_only.count</tt> + # returns the number of garments for which these criteria obtain. + # Similarly with <tt>Shirt.red.dry_clean_only.average(:thread_count)</tt>. + # + # 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, # # class Person < ActiveRecord::Base # has_many :shirts # end # - # then <tt>elton.shirts.red.dry_clean_only</tt> will return all of Elton's red, dry clean - # only shirts. - # - # Named \scopes can also be procedural: - # - # class Shirt < ActiveRecord::Base - # scope :colored, lambda { |color| where(:color => color) } - # end - # - # In this example, <tt>Shirt.colored('puce')</tt> finds all puce shirts. - # - # On Ruby 1.9 you can use the 'stabby lambda' syntax: - # - # scope :colored, ->(color) { where(:color => color) } - # - # Note that scopes defined with \scope will be evaluated when they are defined, rather than - # when they are used. For example, the following would be incorrect: - # - # class Post < ActiveRecord::Base - # scope :recent, where('published_at >= ?', Time.current - 1.week) - # end - # - # The example above would be 'frozen' to the <tt>Time.current</tt> value when the <tt>Post</tt> - # class was defined, and so the resultant SQL query would always be the same. The correct - # way to do this would be via a lambda, which will re-evaluate the scope each time - # it is called: - # - # class Post < ActiveRecord::Base - # scope :recent, lambda { where('published_at >= ?', Time.current - 1.week) } - # end + # 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 <tt>has_many</tt> declarations: + # \Named scopes can also have extensions, just as with +has_many+ + # declarations: # # class Shirt < ActiveRecord::Base - # scope :red, where(:color => 'red') do + # scope :red, -> { where(color: 'red') } do # def dom_id # 'red_shirts' # end @@ -133,18 +116,18 @@ module ActiveRecord # Scopes can also be used while creating/building a record. # # class Article < ActiveRecord::Base - # scope :published, where(:published => true) + # scope :published, -> { where(published: true) } # end # # Article.published.new.published # => true # Article.published.create.published # => true # - # Class methods on your model are automatically available + # \Class methods on your model are automatically available # on scopes. Assuming the following setup: # # class Article < ActiveRecord::Base - # scope :published, where(:published => true) - # scope :featured, where(:featured => true) + # scope :published, -> { where(published: true) } + # scope :featured, -> { where(featured: true) } # # def self.latest_article # order('published_at desc').first diff --git a/activerecord/lib/active_record/serialization.rb b/activerecord/lib/active_record/serialization.rb index e8dd312a47..6b55af4205 100644 --- a/activerecord/lib/active_record/serialization.rb +++ b/activerecord/lib/active_record/serialization.rb @@ -1,19 +1,11 @@ module ActiveRecord #:nodoc: - ActiveSupport.on_load(:active_record_config) do - mattr_accessor :include_root_in_json, instance_accessor: false - self.include_root_in_json = true - end - # = Active Record Serialization module Serialization extend ActiveSupport::Concern include ActiveModel::Serializers::JSON included do - singleton_class.class_eval do - remove_method :include_root_in_json - delegate :include_root_in_json, to: 'ActiveRecord::Model' - end + self.include_root_in_json = true end def serializable_hash(options = nil) diff --git a/activerecord/lib/active_record/serializers/xml_serializer.rb b/activerecord/lib/active_record/serializers/xml_serializer.rb index 834d01a1e8..1a766093d0 100644 --- a/activerecord/lib/active_record/serializers/xml_serializer.rb +++ b/activerecord/lib/active_record/serializers/xml_serializer.rb @@ -36,7 +36,7 @@ module ActiveRecord #:nodoc: # # For instance: # - # topic.to_xml(:skip_instruct => true, :except => [ :id, :bonus_time, :written_on, :replies_count ]) + # topic.to_xml(skip_instruct: true, except: [ :id, :bonus_time, :written_on, :replies_count ]) # # <topic> # <title>The First Topic</title> @@ -50,7 +50,7 @@ module ActiveRecord #:nodoc: # # To include first level associations use <tt>:include</tt>: # - # firm.to_xml :include => [ :account, :clients ] + # firm.to_xml include: [ :account, :clients ] # # <?xml version="1.0" encoding="UTF-8"?> # <firm> @@ -81,7 +81,7 @@ module ActiveRecord #:nodoc: # associated with models. # # proc = Proc.new { |options, record| options[:builder].tag!('name-reverse', record.name.reverse) } - # firm.to_xml :procs => [ proc ] + # firm.to_xml procs: [ proc ] # # <firm> # # ... normal attributes as shown above ... @@ -90,7 +90,7 @@ module ActiveRecord #:nodoc: # # To include deeper levels of associations pass a hash like this: # - # firm.to_xml :include => {:account => {}, :clients => {:include => :address}} + # firm.to_xml include: {account: {}, clients: {include: :address}} # <?xml version="1.0" encoding="UTF-8"?> # <firm> # <id type="integer">1</id> @@ -120,7 +120,7 @@ module ActiveRecord #:nodoc: # # To include any methods on the model being called use <tt>:methods</tt>: # - # firm.to_xml :methods => [ :calculated_earnings, :real_earnings ] + # firm.to_xml methods: [ :calculated_earnings, :real_earnings ] # # <firm> # # ... normal attributes as shown above ... @@ -132,7 +132,7 @@ module ActiveRecord #:nodoc: # modified version of the options hash that was given to +to_xml+: # # proc = Proc.new { |options| options[:builder].tag!('abc', 'def') } - # firm.to_xml :procs => [ proc ] + # firm.to_xml procs: [ proc ] # # <firm> # # ... normal attributes as shown above ... @@ -164,7 +164,7 @@ module ActiveRecord #:nodoc: # def to_xml(options = {}) # require 'builder' # options[:indent] ||= 2 - # xml = options[:builder] ||= ::Builder::XmlMarkup.new(:indent => options[:indent]) + # xml = options[:builder] ||= ::Builder::XmlMarkup.new(indent: options[:indent]) # xml.instruct! unless options[:skip_instruct] # xml.level_one do # xml.tag!(:second_level, 'content') diff --git a/activerecord/lib/active_record/tasks/mysql_database_tasks.rb b/activerecord/lib/active_record/tasks/mysql_database_tasks.rb index 2340f949b7..3d27c97254 100644 --- a/activerecord/lib/active_record/tasks/mysql_database_tasks.rb +++ b/activerecord/lib/active_record/tasks/mysql_database_tasks.rb @@ -74,10 +74,16 @@ module ActiveRecord end def creation_options - { - charset: (configuration['encoding'] || DEFAULT_CHARSET), - collation: (configuration['collation'] || DEFAULT_COLLATION) - } + 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 def error_class diff --git a/activerecord/lib/active_record/timestamp.rb b/activerecord/lib/active_record/timestamp.rb index c32e0d6bf8..cf17b1d8a4 100644 --- a/activerecord/lib/active_record/timestamp.rb +++ b/activerecord/lib/active_record/timestamp.rb @@ -1,10 +1,5 @@ module ActiveRecord - ActiveSupport.on_load(:active_record_config) do - mattr_accessor :record_timestamps, instance_accessor: false - self.record_timestamps = true - end - # = Active Record Timestamp # # Active Record automatically timestamps create and update operations if the @@ -37,16 +32,18 @@ module ActiveRecord extend ActiveSupport::Concern included do - config_attribute :record_timestamps, instance_writer: true + class_attribute :record_timestamps + self.record_timestamps = true end - def initialize_dup(other) + def initialize_dup(other) # :nodoc: clear_timestamp_attributes + super end private - def create #:nodoc: + def create if self.record_timestamps current_time = current_time_from_proper_timezone @@ -60,7 +57,7 @@ module ActiveRecord super end - def update(*args) #:nodoc: + def update(*args) if should_record_timestamps? current_time = current_time_from_proper_timezone @@ -74,7 +71,7 @@ module ActiveRecord end def should_record_timestamps? - self.record_timestamps && (!partial_updates? || changed? || (attributes.keys & self.class.serialized_attributes.keys).present?) + self.record_timestamps && (!partial_writes? || changed? || (attributes.keys & self.class.serialized_attributes.keys).present?) end def timestamp_attributes_for_create_in_model @@ -89,19 +86,19 @@ module ActiveRecord timestamp_attributes_for_create_in_model + timestamp_attributes_for_update_in_model end - def timestamp_attributes_for_update #:nodoc: + def timestamp_attributes_for_update [:updated_at, :updated_on] end - def timestamp_attributes_for_create #:nodoc: + def timestamp_attributes_for_create [:created_at, :created_on] end - def all_timestamp_attributes #:nodoc: + def all_timestamp_attributes timestamp_attributes_for_create + timestamp_attributes_for_update end - def current_time_from_proper_timezone #:nodoc: + def current_time_from_proper_timezone self.class.default_timezone == :utc ? Time.now.utc : Time.now end diff --git a/activerecord/lib/active_record/transactions.rb b/activerecord/lib/active_record/transactions.rb index 934393b4e7..ce6998530f 100644 --- a/activerecord/lib/active_record/transactions.rb +++ b/activerecord/lib/active_record/transactions.rb @@ -108,10 +108,10 @@ module ActiveRecord # # # Suppose that we have a Number model with a unique column called 'i'. # Number.transaction do - # Number.create(:i => 0) + # Number.create(i: 0) # begin # # This will raise a unique constraint error... - # Number.create(:i => 0) + # Number.create(i: 0) # rescue ActiveRecord::StatementInvalid # # ...which we ignore. # end @@ -119,7 +119,7 @@ module ActiveRecord # # On PostgreSQL, the transaction is now unusable. The following # # statement will cause a PostgreSQL error, even though the unique # # constraint is no longer violated: - # Number.create(:i => 1) + # Number.create(i: 1) # # => "PGError: ERROR: current transaction is aborted, commands # # ignored until end of transaction block" # end @@ -134,9 +134,9 @@ module ActiveRecord # transaction. For example, the following behavior may be surprising: # # User.transaction do - # User.create(:username => 'Kotori') + # User.create(username: 'Kotori') # User.transaction do - # User.create(:username => 'Nemu') + # User.create(username: 'Nemu') # raise ActiveRecord::Rollback # end # end @@ -147,14 +147,14 @@ module ActiveRecord # real transaction is committed. # # In order to get a ROLLBACK for the nested transaction you may ask for a real - # sub-transaction by passing <tt>:requires_new => true</tt>. If anything goes wrong, + # sub-transaction by passing <tt>requires_new: true</tt>. If anything goes wrong, # the database rolls back to the beginning of the sub-transaction without rolling # back the parent transaction. If we add it to the previous example: # # User.transaction do - # User.create(:username => 'Kotori') - # User.transaction(:requires_new => true) do - # User.create(:username => 'Nemu') + # User.create(username: 'Kotori') + # User.transaction(requires_new: true) do + # User.create(username: 'Nemu') # raise ActiveRecord::Rollback # end # end @@ -194,7 +194,7 @@ module ActiveRecord # automatically released. The following example demonstrates the problem: # # Model.connection.transaction do # BEGIN - # Model.connection.transaction(:requires_new => true) do # CREATE SAVEPOINT active_record_1 + # Model.connection.transaction(requires_new: true) do # CREATE SAVEPOINT active_record_1 # Model.connection.create_table(...) # active_record_1 now automatically released # end # RELEASE savepoint active_record_1 # # ^^^^ BOOM! database error! @@ -213,13 +213,13 @@ module ActiveRecord # You can specify that the callback should only be fired by a certain action with # the +:on+ option: # - # after_commit :do_foo, :on => :create - # after_commit :do_bar, :on => :update - # after_commit :do_baz, :on => :destroy + # after_commit :do_foo, on: :create + # after_commit :do_bar, on: :update + # after_commit :do_baz, on: :destroy # # Also, to have the callback fired on create and update, but not on destroy: # - # after_commit :do_zoo, :if => :persisted? + # after_commit :do_zoo, if: :persisted? # # Note that transactional fixtures do not play well with this feature. Please # use the +test_after_commit+ gem to have these hooks fired in tests. @@ -328,6 +328,7 @@ module ActiveRecord @_start_transaction_state[:new_record] = @new_record @_start_transaction_state[:destroyed] = @destroyed @_start_transaction_state[:level] = (@_start_transaction_state[:level] || 0) + 1 + @_start_transaction_state[:frozen?] = @attributes.frozen? end # Clear the new record state and id of a record. @@ -342,8 +343,8 @@ module ActiveRecord @_start_transaction_state[:level] = (@_start_transaction_state[:level] || 0) - 1 if @_start_transaction_state[:level] < 1 || force restore_state = @_start_transaction_state - was_frozen = @attributes.frozen? - @attributes = @attributes.dup if was_frozen + was_frozen = restore_state[:frozen?] + @attributes = @attributes.dup if @attributes.frozen? @new_record = restore_state[:new_record] @destroyed = restore_state[:destroyed] if restore_state.has_key?(:id) diff --git a/activerecord/lib/active_record/validations.rb b/activerecord/lib/active_record/validations.rb index ed561bfb3c..3706885881 100644 --- a/activerecord/lib/active_record/validations.rb +++ b/activerecord/lib/active_record/validations.rb @@ -10,8 +10,8 @@ module ActiveRecord # puts invalid.record.errors # end class RecordInvalid < ActiveRecordError - attr_reader :record - def initialize(record) + attr_reader :record # :nodoc: + def initialize(record) # :nodoc: @record = record errors = @record.errors.full_messages.join(", ") super(I18n.t(:"#{@record.class.i18n_scope}.errors.messages.record_invalid", :errors => errors, :default => :"errors.messages.record_invalid")) @@ -44,23 +44,24 @@ module ActiveRecord end end - # 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 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. 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 Base#save but will raise a +RecordInvalid+ + # exception instead of returning +false+ if the record is not valid. def save!(options={}) perform_validations(options) ? super : raise(RecordInvalid.new(self)) end - # Runs all the validations within the specified context. Returns true if no errors are found, - # false otherwise. + # Runs all the validations within the specified context. Returns +true+ if + # no errors are found, +false+ otherwise. # - # If the argument is false (default is +nil+), the context is set to <tt>:create</tt> if - # <tt>new_record?</tt> is true, and to <tt>:update</tt> if it is not. + # If the argument is +false+ (default is +nil+), the context is set to <tt>:create</tt> if + # <tt>new_record?</tt> is +true+, and to <tt>:update</tt> if it is not. # # Validations with no <tt>:on</tt> option will run no matter the context. Validations with # some <tt>:on</tt> option will only run in the specified context. @@ -72,7 +73,7 @@ module ActiveRecord protected - def perform_validations(options={}) + def perform_validations(options={}) # :nodoc: perform_validation = options[:validate] != false perform_validation ? valid?(options[:context]) : true end diff --git a/activerecord/lib/active_record/validations/associated.rb b/activerecord/lib/active_record/validations/associated.rb index 1fa6629980..7f1972ccf9 100644 --- a/activerecord/lib/active_record/validations/associated.rb +++ b/activerecord/lib/active_record/validations/associated.rb @@ -38,7 +38,7 @@ module ActiveRecord # proc or string should return or evaluate to a +true+ or +false+ value. # * <tt>:unless</tt> - Specifies a method, proc or string to call to # determine if the validation should not occur (e.g. <tt>unless: :skip_validation</tt>, - # or <tt>unless: => Proc.new { |user| user.signup_step <= 2 }</tt>). The + # or <tt>unless: Proc.new { |user| user.signup_step <= 2 }</tt>). The # method, proc or string should return or evaluate to a +true+ or +false+ # value. def validates_associated(*attr_names) diff --git a/activerecord/lib/active_record/validations/presence.rb b/activerecord/lib/active_record/validations/presence.rb index 056527b512..6b14c39686 100644 --- a/activerecord/lib/active_record/validations/presence.rb +++ b/activerecord/lib/active_record/validations/presence.rb @@ -1,12 +1,14 @@ module ActiveRecord module Validations - class PresenceValidator < ActiveModel::Validations::PresenceValidator + class PresenceValidator < ActiveModel::Validations::PresenceValidator # :nodoc: def validate(record) super attributes.each do |attribute| next unless record.class.reflect_on_association(attribute) - value = record.send(attribute) - if Array(value).all? { |r| r.marked_for_destruction? } + associated_records = Array(record.send(attribute)) + + # Superclass validates presence. Ensure present records aren't about to be destroyed. + if associated_records.present? && associated_records.all? { |r| r.marked_for_destruction? } record.errors.add(attribute, :blank, options) end end @@ -29,7 +31,7 @@ module ActiveRecord # # If you want to validate the presence of a boolean field (where the real values # are true and false), you will want to use - # <tt>validates_inclusion_of :field_name, :in => [true, false]</tt>. + # <tt>validates_inclusion_of :field_name, in: [true, false]</tt>. # # This is due to the way Object#blank? handles boolean values: # <tt>false.blank? # => true</tt>. @@ -46,16 +48,15 @@ module ActiveRecord # validation contexts by default (+nil+), other options are <tt>:create</tt> # and <tt>:update</tt>. # * <tt>:if</tt> - Specifies a method, proc or string to call to determine if - # the validation should occur (e.g. <tt>:if => :allow_validation</tt>, or - # <tt>:if => Proc.new { |user| user.signup_step > 2 }</tt>). The method, proc - # or string should return or evaluate to a true or false value. + # the validation should occur (e.g. <tt>if: :allow_validation</tt>, or + # <tt>if: Proc.new { |user| user.signup_step > 2 }</tt>). The method, proc + # or string should return or evaluate to a +true+ or +false+ value. # * <tt>:unless</tt> - Specifies a method, proc or string to call to determine - # if the validation should not occur (e.g. <tt>:unless => :skip_validation</tt>, - # or <tt>:unless => Proc.new { |user| user.signup_step <= 2 }</tt>). The method, - # proc or string should return or evaluate to a true or false value. + # if the validation should not occur (e.g. <tt>unless: :skip_validation</tt>, + # or <tt>unless: Proc.new { |user| user.signup_step <= 2 }</tt>). The method, + # proc or string should return or evaluate to a +true+ or +false+ value. # * <tt>:strict</tt> - Specifies whether validation should be strict. # See <tt>ActiveModel::Validation#validates!</tt> for more information. - # def validates_presence_of(*attr_names) validates_with PresenceValidator, _merge_attributes(attr_names) end diff --git a/activerecord/lib/active_record/validations/uniqueness.rb b/activerecord/lib/active_record/validations/uniqueness.rb index c117872ac8..5fa6a0b892 100644 --- a/activerecord/lib/active_record/validations/uniqueness.rb +++ b/activerecord/lib/active_record/validations/uniqueness.rb @@ -2,7 +2,7 @@ require 'active_support/core_ext/array/prepend_and_append' module ActiveRecord module Validations - class UniquenessValidator < ActiveModel::EachValidator #:nodoc: + class UniquenessValidator < ActiveModel::EachValidator # :nodoc: def initialize(options) super(options.reverse_merge(:case_sensitive => true)) end @@ -26,11 +26,12 @@ module ActiveRecord relation = relation.and(table[finder_class.primary_key.to_sym].not_eq(record.send(:id))) if record.persisted? Array(options[:scope]).each do |scope_item| - scope_value = record.read_attribute(scope_item) reflection = record.class.reflect_on_association(scope_item) if reflection scope_value = record.send(reflection.foreign_key) scope_item = reflection.foreign_key + else + scope_value = record.read_attribute(scope_item) end relation = relation.and(table[scope_item].eq(scope_value)) end @@ -64,14 +65,12 @@ module ActiveRecord end def build_relation(klass, table, attribute, value) #:nodoc: - reflection = klass.reflect_on_association(attribute) - if reflection - column = klass.columns_hash[reflection.foreign_key] + if reflection = klass.reflect_on_association(attribute) attribute = reflection.foreign_key value = value.attributes[reflection.primary_key_column.name] - else - column = klass.columns_hash[attribute.to_s] end + + column = klass.columns_hash[attribute.to_s] value = column.limit ? value.to_s[0, column.limit] : value.to_s if !value.nil? && column.text? if !options[:case_sensitive] && value && column.text? @@ -199,7 +198,7 @@ module ActiveRecord # can catch it and restart the transaction (e.g. by telling the user # that the title already exists, and asking him to re-enter the title). # This technique is also known as optimistic concurrency control: - # http://en.wikipedia.org/wiki/Optimistic_concurrency_control + # http://en.wikipedia.org/wiki/Optimistic_concurrency_control. # # The bundled ActiveRecord::ConnectionAdapters distinguish unique index # constraint errors from other types of database errors by throwing an @@ -209,10 +208,10 @@ module ActiveRecord # # The following bundled adapters throw the ActiveRecord::RecordNotUnique exception: # - # * ActiveRecord::ConnectionAdapters::MysqlAdapter - # * ActiveRecord::ConnectionAdapters::Mysql2Adapter - # * ActiveRecord::ConnectionAdapters::SQLite3Adapter - # * ActiveRecord::ConnectionAdapters::PostgreSQLAdapter + # * ActiveRecord::ConnectionAdapters::MysqlAdapter. + # * ActiveRecord::ConnectionAdapters::Mysql2Adapter. + # * ActiveRecord::ConnectionAdapters::SQLite3Adapter. + # * ActiveRecord::ConnectionAdapters::PostgreSQLAdapter. def validates_uniqueness_of(*attr_names) validates_with UniquenessValidator, _merge_attributes(attr_names) end |