diff options
Diffstat (limited to 'activerecord/lib/active_record')
237 files changed, 13824 insertions, 11795 deletions
diff --git a/activerecord/lib/active_record/aggregations.rb b/activerecord/lib/active_record/aggregations.rb index f7b50cd25a..10cbd5429c 100644 --- a/activerecord/lib/active_record/aggregations.rb +++ b/activerecord/lib/active_record/aggregations.rb @@ -1,6 +1,6 @@ module ActiveRecord - # = Active Record Aggregations - module Aggregations # :nodoc: + # See ActiveRecord::Aggregations::ClassMethods for documentation + module Aggregations extend ActiveSupport::Concern def initialize_dup(*) # :nodoc: @@ -15,265 +15,268 @@ module ActiveRecord private - def clear_aggregation_cache # :nodoc: + def clear_aggregation_cache @aggregation_cache.clear if persisted? end - def init_internals # :nodoc: + def init_internals @aggregation_cache = {} super end - # Active Record implements aggregation through a macro-like class method called +composed_of+ - # for representing attributes as value objects. It expresses relationships like "Account [is] - # composed of Money [among other things]" or "Person [is] composed of [an] address". Each call - # to the macro adds a description of how the value objects are created from the attributes of - # the entity object (when the entity is initialized either as a new object or from finding an - # existing object) and how it can be turned back into attributes (when the entity is saved to - # 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) ] - # end - # - # The customer class now has the following methods to manipulate the value objects: - # * <tt>Customer#balance, Customer#balance=(money)</tt> - # * <tt>Customer#address, Customer#address=(address)</tt> - # - # These methods will operate with value objects like the ones described below: - # - # class Money - # include Comparable - # attr_reader :amount, :currency - # EXCHANGE_RATES = { "USD_TO_DKK" => 6 } - # - # def initialize(amount, currency = "USD") - # @amount, @currency = amount, currency - # end - # - # def exchange_to(other_currency) - # exchanged_amount = (amount * EXCHANGE_RATES["#{currency}_TO_#{other_currency}"]).floor - # Money.new(exchanged_amount, other_currency) - # end - # - # def ==(other_money) - # amount == other_money.amount && currency == other_money.currency - # end - # - # def <=>(other_money) - # if currency == other_money.currency - # amount <=> other_money.amount - # else - # amount <=> other_money.exchange_to(currency).amount - # end - # end - # end - # - # class Address - # attr_reader :street, :city - # def initialize(street, city) - # @street, @city = street, city - # end - # - # def close_to?(other_address) - # city == other_address.city - # end - # - # def ==(other_address) - # city == other_address.city && street == other_address.street - # end - # end - # - # Now it's possible to access attributes from the database through the value objects instead. If - # you choose to name the composition the same as the attribute's name, it will be the only way to - # access that attribute. That's the case with our +balance+ attribute. You interact with the value - # objects just like you would with any other attribute: - # - # customer.balance = Money.new(20) # sets the Money value object and the attribute - # customer.balance # => Money value object - # customer.balance.exchange_to("DKK") # => Money.new(120, "DKK") - # customer.balance > Money.new(10) # => true - # customer.balance == Money.new(20) # => true - # customer.balance < Money.new(5) # => false - # - # Value objects can also be composed of multiple attributes, such as the case of Address. The order - # of the mappings will determine the order of the parameters. - # - # customer.address_street = "Hyancintvej" - # customer.address_city = "Copenhagen" - # customer.address # => Address.new("Hyancintvej", "Copenhagen") - # - # customer.address = Address.new("May Street", "Chicago") - # customer.address_street # => "May Street" - # customer.address_city # => "Chicago" - # - # == Writing value objects - # - # Value objects are immutable and interchangeable objects that represent a given value, such as - # a Money object representing $5. Two Money objects both representing $5 should be equal (through - # methods such as <tt>==</tt> and <tt><=></tt> from Comparable if ranking makes sense). This is - # unlike entity objects where equality is determined by identity. An entity class such as Customer can - # easily have two different objects that both have an address on Hyancintvej. Entity identity is - # determined by object or relational unique identifiers (such as primary keys). Normal - # ActiveRecord::Base classes are entity objects. - # - # It's also important to treat the value objects as immutable. Don't allow the Money object to have - # its amount changed after creation. Create a new Money object with the new value instead. The - # Money#exchange_to method is an example of this. It returns a new value object instead of changing - # its own values. Active Record won't persist value objects that have been changed through means - # other than the writer method. - # - # The immutable requirement is enforced by Active Record by freezing any object assigned as a value - # object. Attempting to change it afterwards will result in a RuntimeError. - # - # Read more about value objects on http://c2.com/cgi/wiki?ValueObject and on the dangers of not - # keeping value objects immutable on http://c2.com/cgi/wiki?ValueObjectsShouldBeImmutable - # - # == Custom constructors and converters - # - # By default value objects are initialized by calling the <tt>new</tt> constructor of the value - # class passing each of the mapped attributes, in the order specified by the <tt>:mapping</tt> - # option, as arguments. If the value class doesn't support this convention then +composed_of+ allows - # a custom constructor to be specified. - # - # When a new value is assigned to the value object, the default assumption is that the new value - # is an instance of the value class. Specifying a custom converter allows the new value to be automatically - # converted to an instance of value class if necessary. - # - # For example, the NetworkResource model has +network_address+ and +cidr_range+ attributes that should be - # aggregated using the NetAddr::CIDR value class (http://www.rubydoc.info/gems/netaddr/1.5.0/NetAddr/CIDR). - # The constructor for the value class is called +create+ and it expects a CIDR address string as a parameter. - # New values can be assigned to the value object using either another NetAddr::CIDR object, a string - # or an array. The <tt>:constructor</tt> and <tt>:converter</tt> options can be used to meet - # these requirements: - # - # class NetworkResource < ActiveRecord::Base - # 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) } - # end - # - # # This calls the :constructor - # 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 ] - # network_resource.cidr = '192.168.0.1/24' - # - # # This assignment won't use the :converter as the value is already an instance of the value class - # network_resource.cidr = NetAddr::CIDR.create('192.168.2.1/8') - # - # # Saving and then reloading will use the :constructor on reload - # network_resource.save - # network_resource.reload - # - # == Finding records by a value object - # - # Once a +composed_of+ relationship is specified for a model, records can be loaded from the database - # by specifying an instance of the value object in the conditions hash. The following example - # finds all customers with +balance_amount+ equal to 20 and +balance_currency+ equal to "USD": - # - # Customer.where(balance: Money.new(20, "USD")) - # - module ClassMethods - # Adds reader and writer methods for manipulating a value object: - # <tt>composed_of :address</tt> adds <tt>address</tt> and <tt>address=(new_address)</tt> methods. - # - # Options are: - # * <tt>:class_name</tt> - Specifies the class name of the association. Use it only if that name - # can't be inferred from the part id. So <tt>composed_of :address</tt> will by default be linked - # to the Address class, but if the real class name is CompanyAddress, you'll have to specify it - # with this option. - # * <tt>:mapping</tt> - Specifies the mapping of entity attributes to attributes of the value - # object. Each mapping is represented as an array where the first item is the name of the - # entity attribute and the second item is the name of the attribute in the value object. The - # order in which mappings are defined determines the order in which attributes are sent to the - # value class constructor. - # * <tt>:allow_nil</tt> - Specifies that the value object will not be instantiated when all mapped - # attributes are +nil+. Setting the value object to +nil+ has the effect of writing +nil+ to all - # mapped attributes. - # This defaults to +false+. - # * <tt>:constructor</tt> - A symbol specifying the name of the constructor method or a Proc that - # is called to initialize the value object. The constructor is passed all of the mapped attributes, - # in the order that they are defined in the <tt>:mapping option</tt>, as arguments and uses them - # to instantiate a <tt>:class_name</tt> object. - # The default is <tt>:new</tt>. - # * <tt>:converter</tt> - A symbol specifying the name of a class method of <tt>:class_name</tt> - # or a Proc that is called when a new value is assigned to the value object. The converter is - # passed the single value that is used in the assignment and is only called if the new value is - # not an instance of <tt>:class_name</tt>. If <tt>:allow_nil</tt> is set to true, the converter - # can return nil to skip the assignment. - # - # Option examples: - # composed_of :temperature, mapping: %w(reading celsius) - # 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 :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) } - # - def composed_of(part_id, options = {}) - options.assert_valid_keys(:class_name, :mapping, :allow_nil, :constructor, :converter) + # Active Record implements aggregation through a macro-like class method called #composed_of + # for representing attributes as value objects. It expresses relationships like "Account [is] + # composed of Money [among other things]" or "Person [is] composed of [an] address". Each call + # to the macro adds a description of how the value objects are created from the attributes of + # the entity object (when the entity is initialized either as a new object or from finding an + # existing object) and how it can be turned back into attributes (when the entity is saved to + # the database). + # + # class Customer < ActiveRecord::Base + # composed_of :balance, class_name: "Money", mapping: %w(amount currency) + # 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: + # * <tt>Customer#balance, Customer#balance=(money)</tt> + # * <tt>Customer#address, Customer#address=(address)</tt> + # + # These methods will operate with value objects like the ones described below: + # + # class Money + # include Comparable + # attr_reader :amount, :currency + # EXCHANGE_RATES = { "USD_TO_DKK" => 6 } + # + # def initialize(amount, currency = "USD") + # @amount, @currency = amount, currency + # end + # + # def exchange_to(other_currency) + # exchanged_amount = (amount * EXCHANGE_RATES["#{currency}_TO_#{other_currency}"]).floor + # Money.new(exchanged_amount, other_currency) + # end + # + # def ==(other_money) + # amount == other_money.amount && currency == other_money.currency + # end + # + # def <=>(other_money) + # if currency == other_money.currency + # amount <=> other_money.amount + # else + # amount <=> other_money.exchange_to(currency).amount + # end + # end + # end + # + # class Address + # attr_reader :street, :city + # def initialize(street, city) + # @street, @city = street, city + # end + # + # def close_to?(other_address) + # city == other_address.city + # end + # + # def ==(other_address) + # city == other_address.city && street == other_address.street + # end + # end + # + # Now it's possible to access attributes from the database through the value objects instead. If + # you choose to name the composition the same as the attribute's name, it will be the only way to + # access that attribute. That's the case with our +balance+ attribute. You interact with the value + # objects just like you would with any other attribute: + # + # customer.balance = Money.new(20) # sets the Money value object and the attribute + # customer.balance # => Money value object + # customer.balance.exchange_to("DKK") # => Money.new(120, "DKK") + # customer.balance > Money.new(10) # => true + # customer.balance == Money.new(20) # => true + # customer.balance < Money.new(5) # => false + # + # Value objects can also be composed of multiple attributes, such as the case of Address. The order + # of the mappings will determine the order of the parameters. + # + # customer.address_street = "Hyancintvej" + # customer.address_city = "Copenhagen" + # customer.address # => Address.new("Hyancintvej", "Copenhagen") + # + # customer.address = Address.new("May Street", "Chicago") + # customer.address_street # => "May Street" + # customer.address_city # => "Chicago" + # + # == Writing value objects + # + # Value objects are immutable and interchangeable objects that represent a given value, such as + # a Money object representing $5. Two Money objects both representing $5 should be equal (through + # methods such as <tt>==</tt> and <tt><=></tt> from Comparable if ranking makes sense). This is + # unlike entity objects where equality is determined by identity. An entity class such as Customer can + # easily have two different objects that both have an address on Hyancintvej. Entity identity is + # determined by object or relational unique identifiers (such as primary keys). Normal + # ActiveRecord::Base classes are entity objects. + # + # It's also important to treat the value objects as immutable. Don't allow the Money object to have + # its amount changed after creation. Create a new Money object with the new value instead. The + # <tt>Money#exchange_to</tt> method is an example of this. It returns a new value object instead of changing + # its own values. Active Record won't persist value objects that have been changed through means + # other than the writer method. + # + # The immutable requirement is enforced by Active Record by freezing any object assigned as a value + # object. Attempting to change it afterwards will result in a +RuntimeError+. + # + # Read more about value objects on http://c2.com/cgi/wiki?ValueObject and on the dangers of not + # keeping value objects immutable on http://c2.com/cgi/wiki?ValueObjectsShouldBeImmutable + # + # == Custom constructors and converters + # + # By default value objects are initialized by calling the <tt>new</tt> constructor of the value + # class passing each of the mapped attributes, in the order specified by the <tt>:mapping</tt> + # option, as arguments. If the value class doesn't support this convention then #composed_of allows + # a custom constructor to be specified. + # + # When a new value is assigned to the value object, the default assumption is that the new value + # is an instance of the value class. Specifying a custom converter allows the new value to be automatically + # converted to an instance of value class if necessary. + # + # For example, the +NetworkResource+ model has +network_address+ and +cidr_range+ attributes that should be + # aggregated using the +NetAddr::CIDR+ value class (http://www.rubydoc.info/gems/netaddr/1.5.0/NetAddr/CIDR). + # The constructor for the value class is called +create+ and it expects a CIDR address string as a parameter. + # New values can be assigned to the value object using either another +NetAddr::CIDR+ object, a string + # or an array. The <tt>:constructor</tt> and <tt>:converter</tt> options can be used to meet + # these requirements: + # + # class NetworkResource < ActiveRecord::Base + # 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) } + # end + # + # # This calls the :constructor + # 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 ] + # network_resource.cidr = '192.168.0.1/24' + # + # # This assignment won't use the :converter as the value is already an instance of the value class + # network_resource.cidr = NetAddr::CIDR.create('192.168.2.1/8') + # + # # Saving and then reloading will use the :constructor on reload + # network_resource.save + # network_resource.reload + # + # == Finding records by a value object + # + # Once a #composed_of relationship is specified for a model, records can be loaded from the database + # by specifying an instance of the value object in the conditions hash. The following example + # finds all customers with +balance_amount+ equal to 20 and +balance_currency+ equal to "USD": + # + # Customer.where(balance: Money.new(20, "USD")) + # + module ClassMethods + # Adds reader and writer methods for manipulating a value object: + # <tt>composed_of :address</tt> adds <tt>address</tt> and <tt>address=(new_address)</tt> methods. + # + # Options are: + # * <tt>:class_name</tt> - Specifies the class name of the association. Use it only if that name + # can't be inferred from the part id. So <tt>composed_of :address</tt> will by default be linked + # to the Address class, but if the real class name is +CompanyAddress+, you'll have to specify it + # with this option. + # * <tt>:mapping</tt> - Specifies the mapping of entity attributes to attributes of the value + # object. Each mapping is represented as an array where the first item is the name of the + # entity attribute and the second item is the name of the attribute in the value object. The + # order in which mappings are defined determines the order in which attributes are sent to the + # value class constructor. + # * <tt>:allow_nil</tt> - Specifies that the value object will not be instantiated when all mapped + # attributes are +nil+. Setting the value object to +nil+ has the effect of writing +nil+ to all + # mapped attributes. + # This defaults to +false+. + # * <tt>:constructor</tt> - A symbol specifying the name of the constructor method or a Proc that + # is called to initialize the value object. The constructor is passed all of the mapped attributes, + # in the order that they are defined in the <tt>:mapping option</tt>, as arguments and uses them + # to instantiate a <tt>:class_name</tt> object. + # The default is <tt>:new</tt>. + # * <tt>:converter</tt> - A symbol specifying the name of a class method of <tt>:class_name</tt> + # or a Proc that is called when a new value is assigned to the value object. The converter is + # passed the single value that is used in the assignment and is only called if the new value is + # not an instance of <tt>:class_name</tt>. If <tt>:allow_nil</tt> is set to true, the converter + # can return +nil+ to skip the assignment. + # + # Option examples: + # composed_of :temperature, mapping: %w(reading celsius) + # 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 :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) } + # + def composed_of(part_id, options = {}) + options.assert_valid_keys(:class_name, :mapping, :allow_nil, :constructor, :converter) - name = part_id.id2name - class_name = options[:class_name] || name.camelize - mapping = options[:mapping] || [ name, name ] - mapping = [ mapping ] unless mapping.first.is_a?(Array) - allow_nil = options[:allow_nil] || false - constructor = options[:constructor] || :new - converter = options[:converter] + name = part_id.id2name + class_name = options[:class_name] || name.camelize + mapping = options[:mapping] || [ name, name ] + mapping = [ mapping ] unless mapping.first.is_a?(Array) + allow_nil = options[:allow_nil] || false + constructor = options[:constructor] || :new + converter = options[:converter] - reader_method(name, class_name, mapping, allow_nil, constructor) - writer_method(name, class_name, mapping, allow_nil, converter) + reader_method(name, class_name, mapping, allow_nil, constructor) + writer_method(name, class_name, mapping, allow_nil, converter) - reflection = ActiveRecord::Reflection.create(:composed_of, part_id, nil, options, self) - Reflection.add_aggregate_reflection self, part_id, reflection - end + reflection = ActiveRecord::Reflection.create(:composed_of, part_id, nil, options, self) + Reflection.add_aggregate_reflection self, part_id, reflection + end - private - def reader_method(name, class_name, mapping, allow_nil, constructor) - define_method(name) do - if @aggregation_cache[name].nil? && (!allow_nil || mapping.any? {|key, _| !_read_attribute(key).nil? }) - attrs = mapping.collect {|key, _| _read_attribute(key)} - object = constructor.respond_to?(:call) ? - constructor.call(*attrs) : - class_name.constantize.send(constructor, *attrs) - @aggregation_cache[name] = object + private + def reader_method(name, class_name, mapping, allow_nil, constructor) + define_method(name) do + if @aggregation_cache[name].nil? && (!allow_nil || mapping.any? { |key, _| !_read_attribute(key).nil? }) + attrs = mapping.collect { |key, _| _read_attribute(key) } + object = constructor.respond_to?(:call) ? + constructor.call(*attrs) : + class_name.constantize.send(constructor, *attrs) + @aggregation_cache[name] = object + end + @aggregation_cache[name] end - @aggregation_cache[name] end - end - def writer_method(name, class_name, mapping, allow_nil, converter) - define_method("#{name}=") do |part| - klass = class_name.constantize - if part.is_a?(Hash) - raise ArgumentError unless part.size == part.keys.max - part = klass.new(*part.sort.map(&:last)) - end + def writer_method(name, class_name, mapping, allow_nil, converter) + define_method("#{name}=") do |part| + klass = class_name.constantize - unless part.is_a?(klass) || converter.nil? || part.nil? - part = converter.respond_to?(:call) ? converter.call(part) : klass.send(converter, part) - end + unless part.is_a?(klass) || converter.nil? || part.nil? + part = converter.respond_to?(:call) ? converter.call(part) : klass.send(converter, part) + end + + hash_from_multiparameter_assignment = part.is_a?(Hash) && + part.each_key.all? { |k| k.is_a?(Integer) } + if hash_from_multiparameter_assignment + raise ArgumentError unless part.size == part.each_key.max + part = klass.new(*part.sort.map(&:last)) + end - if part.nil? && allow_nil - mapping.each { |key, _| self[key] = nil } - @aggregation_cache[name] = nil - else - mapping.each { |key, value| self[key] = part.send(value) } - @aggregation_cache[name] = part.freeze + if part.nil? && allow_nil + mapping.each { |key, _| self[key] = nil } + @aggregation_cache[name] = nil + else + mapping.each { |key, value| self[key] = part.send(value) } + @aggregation_cache[name] = part.freeze + end end end - end - end + end end end diff --git a/activerecord/lib/active_record/association_relation.rb b/activerecord/lib/active_record/association_relation.rb index ee0bb8fafe..de2d03cd0b 100644 --- a/activerecord/lib/active_record/association_relation.rb +++ b/activerecord/lib/active_record/association_relation.rb @@ -10,7 +10,7 @@ module ActiveRecord end def ==(other) - other == to_a + other == records end def build(*args, &block) @@ -28,8 +28,11 @@ module ActiveRecord private - def exec_queries - super.each { |r| @association.set_inverse_instance r } - end + def exec_queries + super do |r| + @association.set_inverse_instance r + yield r if block_given? + end + end end end diff --git a/activerecord/lib/active_record/associations.rb b/activerecord/lib/active_record/associations.rb index 82cb3fed59..d97d4c7ed0 100644 --- a/activerecord/lib/active_record/associations.rb +++ b/activerecord/lib/active_record/associations.rb @@ -1,99 +1,189 @@ -require 'active_support/core_ext/enumerable' -require 'active_support/core_ext/string/conversions' -require 'active_support/core_ext/module/remove_method' -require 'active_record/errors' +require "active_support/core_ext/enumerable" +require "active_support/core_ext/string/conversions" +require "active_support/core_ext/module/remove_method" +require "active_record/errors" module ActiveRecord class AssociationNotFoundError < ConfigurationError #:nodoc: - def initialize(record, association_name) - super("Association named '#{association_name}' was not found on #{record.class.name}; perhaps you misspelled it?") + def initialize(record = nil, association_name = nil) + if record && association_name + super("Association named '#{association_name}' was not found on #{record.class.name}; perhaps you misspelled it?") + else + super("Association was not found.") + end end end class InverseOfAssociationNotFoundError < ActiveRecordError #:nodoc: - def initialize(reflection, associated_class = nil) - super("Could not find the inverse association for #{reflection.name} (#{reflection.options[:inverse_of].inspect} in #{associated_class.nil? ? reflection.class_name : associated_class.name})") + def initialize(reflection = nil, associated_class = nil) + if reflection + super("Could not find the inverse association for #{reflection.name} (#{reflection.options[:inverse_of].inspect} in #{associated_class.nil? ? reflection.class_name : associated_class.name})") + else + super("Could not find the inverse association.") + end end end class HasManyThroughAssociationNotFoundError < ActiveRecordError #:nodoc: - def initialize(owner_class_name, reflection) - super("Could not find the association #{reflection.options[:through].inspect} in model #{owner_class_name}") + def initialize(owner_class_name = nil, reflection = nil) + if owner_class_name && reflection + super("Could not find the association #{reflection.options[:through].inspect} in model #{owner_class_name}") + else + super("Could not find the association.") + end end end class HasManyThroughAssociationPolymorphicSourceError < ActiveRecordError #:nodoc: - def initialize(owner_class_name, reflection, source_reflection) - super("Cannot have a has_many :through association '#{owner_class_name}##{reflection.name}' on the polymorphic object '#{source_reflection.class_name}##{source_reflection.name}' without 'source_type'. Try adding 'source_type: \"#{reflection.name.to_s.classify}\"' to 'has_many :through' definition.") + def initialize(owner_class_name = nil, reflection = nil, source_reflection = nil) + if owner_class_name && reflection && source_reflection + super("Cannot have a has_many :through association '#{owner_class_name}##{reflection.name}' on the polymorphic object '#{source_reflection.class_name}##{source_reflection.name}' without 'source_type'. Try adding 'source_type: \"#{reflection.name.to_s.classify}\"' to 'has_many :through' definition.") + else + super("Cannot have a has_many :through association.") + end end end class HasManyThroughAssociationPolymorphicThroughError < ActiveRecordError #:nodoc: - def initialize(owner_class_name, reflection) - super("Cannot have a has_many :through association '#{owner_class_name}##{reflection.name}' which goes through the polymorphic association '#{owner_class_name}##{reflection.through_reflection.name}'.") + def initialize(owner_class_name = nil, reflection = nil) + if owner_class_name && reflection + super("Cannot have a has_many :through association '#{owner_class_name}##{reflection.name}' which goes through the polymorphic association '#{owner_class_name}##{reflection.through_reflection.name}'.") + else + super("Cannot have a has_many :through association.") + end end end class HasManyThroughAssociationPointlessSourceTypeError < ActiveRecordError #:nodoc: - def initialize(owner_class_name, reflection, source_reflection) - super("Cannot have a has_many :through association '#{owner_class_name}##{reflection.name}' with a :source_type option if the '#{reflection.through_reflection.class_name}##{source_reflection.name}' is not polymorphic. Try removing :source_type on your association.") + def initialize(owner_class_name = nil, reflection = nil, source_reflection = nil) + if owner_class_name && reflection && source_reflection + super("Cannot have a has_many :through association '#{owner_class_name}##{reflection.name}' with a :source_type option if the '#{reflection.through_reflection.class_name}##{source_reflection.name}' is not polymorphic. Try removing :source_type on your association.") + else + super("Cannot have a has_many :through association.") + end end end class HasOneThroughCantAssociateThroughCollection < ActiveRecordError #:nodoc: - def initialize(owner_class_name, reflection, through_reflection) - super("Cannot have a has_one :through association '#{owner_class_name}##{reflection.name}' where the :through association '#{owner_class_name}##{through_reflection.name}' is a collection. Specify a has_one or belongs_to association in the :through option instead.") + def initialize(owner_class_name = nil, reflection = nil, through_reflection = nil) + if owner_class_name && reflection && through_reflection + super("Cannot have a has_one :through association '#{owner_class_name}##{reflection.name}' where the :through association '#{owner_class_name}##{through_reflection.name}' is a collection. Specify a has_one or belongs_to association in the :through option instead.") + else + super("Cannot have a has_one :through association.") + end end end class HasOneAssociationPolymorphicThroughError < ActiveRecordError #:nodoc: - def initialize(owner_class_name, reflection) - super("Cannot have a has_one :through association '#{owner_class_name}##{reflection.name}' which goes through the polymorphic association '#{owner_class_name}##{reflection.through_reflection.name}'.") + def initialize(owner_class_name = nil, reflection = nil) + if owner_class_name && reflection + super("Cannot have a has_one :through association '#{owner_class_name}##{reflection.name}' which goes through the polymorphic association '#{owner_class_name}##{reflection.through_reflection.name}'.") + else + super("Cannot have a has_one :through association.") + end end end class HasManyThroughSourceAssociationNotFoundError < ActiveRecordError #:nodoc: - def initialize(reflection) - through_reflection = reflection.through_reflection - source_reflection_names = reflection.source_reflection_names - source_associations = reflection.through_reflection.klass._reflections.keys - super("Could not find the source association(s) #{source_reflection_names.collect(&:inspect).to_sentence(:two_words_connector => ' or ', :last_word_connector => ', or ', :locale => :en)} in model #{through_reflection.klass}. Try 'has_many #{reflection.name.inspect}, :through => #{through_reflection.name.inspect}, :source => <name>'. Is it one of #{source_associations.to_sentence(:two_words_connector => ' or ', :last_word_connector => ', or ', :locale => :en)}?") + def initialize(reflection = nil) + if reflection + through_reflection = reflection.through_reflection + source_reflection_names = reflection.source_reflection_names + source_associations = reflection.through_reflection.klass._reflections.keys + super("Could not find the source association(s) #{source_reflection_names.collect(&:inspect).to_sentence(two_words_connector: ' or ', last_word_connector: ', or ', locale: :en)} in model #{through_reflection.klass}. Try 'has_many #{reflection.name.inspect}, :through => #{through_reflection.name.inspect}, :source => <name>'. Is it one of #{source_associations.to_sentence(two_words_connector: ' or ', last_word_connector: ', or ', locale: :en)}?") + else + super("Could not find the source association(s).") + end + end + end + + class ThroughCantAssociateThroughHasOneOrManyReflection < ActiveRecordError #:nodoc: + def initialize(owner = nil, reflection = nil) + if owner && reflection + super("Cannot modify association '#{owner.class.name}##{reflection.name}' because the source reflection class '#{reflection.source_reflection.class_name}' is associated to '#{reflection.through_reflection.class_name}' via :#{reflection.source_reflection.macro}.") + else + super("Cannot modify association.") + end end end - class HasManyThroughCantAssociateThroughHasOneOrManyReflection < ActiveRecordError #:nodoc: - def initialize(owner, reflection) - super("Cannot modify association '#{owner.class.name}##{reflection.name}' because the source reflection class '#{reflection.source_reflection.class_name}' is associated to '#{reflection.through_reflection.class_name}' via :#{reflection.source_reflection.macro}.") + class AmbiguousSourceReflectionForThroughAssociation < ActiveRecordError # :nodoc: + def initialize(klass, macro, association_name, options, possible_sources) + example_options = options.dup + example_options[:source] = possible_sources.first + + super("Ambiguous source reflection for through association. Please " \ + "specify a :source directive on your declaration like:\n" \ + "\n" \ + " class #{klass} < ActiveRecord::Base\n" \ + " #{macro} :#{association_name}, #{example_options}\n" \ + " end" + ) end end + class HasManyThroughCantAssociateThroughHasOneOrManyReflection < ThroughCantAssociateThroughHasOneOrManyReflection #:nodoc: + end + + class HasOneThroughCantAssociateThroughHasOneOrManyReflection < ThroughCantAssociateThroughHasOneOrManyReflection #:nodoc: + end + class HasManyThroughCantAssociateNewRecords < ActiveRecordError #:nodoc: - def initialize(owner, reflection) - super("Cannot associate new records through '#{owner.class.name}##{reflection.name}' on '#{reflection.source_reflection.class_name rescue nil}##{reflection.source_reflection.name rescue nil}'. Both records must have an id in order to create the has_many :through record associating them.") + def initialize(owner = nil, reflection = nil) + if owner && reflection + super("Cannot associate new records through '#{owner.class.name}##{reflection.name}' on '#{reflection.source_reflection.class_name rescue nil}##{reflection.source_reflection.name rescue nil}'. Both records must have an id in order to create the has_many :through record associating them.") + else + super("Cannot associate new records.") + end end end class HasManyThroughCantDissociateNewRecords < ActiveRecordError #:nodoc: - def initialize(owner, reflection) - super("Cannot dissociate new records through '#{owner.class.name}##{reflection.name}' on '#{reflection.source_reflection.class_name rescue nil}##{reflection.source_reflection.name rescue nil}'. Both records must have an id in order to delete the has_many :through record associating them.") + def initialize(owner = nil, reflection = nil) + if owner && reflection + super("Cannot dissociate new records through '#{owner.class.name}##{reflection.name}' on '#{reflection.source_reflection.class_name rescue nil}##{reflection.source_reflection.name rescue nil}'. Both records must have an id in order to delete the has_many :through record associating them.") + else + super("Cannot dissociate new records.") + end end end - class HasManyThroughNestedAssociationsAreReadonly < ActiveRecordError #:nodoc: - def initialize(owner, reflection) - super("Cannot modify association '#{owner.class.name}##{reflection.name}' because it goes through more than one other association.") + class ThroughNestedAssociationsAreReadonly < ActiveRecordError #:nodoc: + def initialize(owner = nil, reflection = nil) + if owner && reflection + super("Cannot modify association '#{owner.class.name}##{reflection.name}' because it goes through more than one other association.") + else + super("Through nested associations are read-only.") + end end end - class EagerLoadPolymorphicError < ActiveRecordError #:nodoc: - def initialize(reflection) - super("Cannot eagerly load the polymorphic association #{reflection.name.inspect}") + class HasManyThroughNestedAssociationsAreReadonly < ThroughNestedAssociationsAreReadonly #:nodoc: + end + + class HasOneThroughNestedAssociationsAreReadonly < ThroughNestedAssociationsAreReadonly #:nodoc: + end + + # This error is raised when trying to eager load a polymorphic association using a JOIN. + # Eager loading polymorphic associations is only possible with + # {ActiveRecord::Relation#preload}[rdoc-ref:QueryMethods#preload]. + class EagerLoadPolymorphicError < ActiveRecordError + def initialize(reflection = nil) + if reflection + super("Cannot eagerly load the polymorphic association #{reflection.name.inspect}") + else + super("Eager load polymorphic error.") + end end end class ReadOnlyAssociation < ActiveRecordError #:nodoc: - def initialize(reflection) - super("Cannot add to a has_many :through association. Try adding to #{reflection.through_reflection.name.inspect}.") + def initialize(reflection = nil) + if reflection + super("Cannot add to a has_many :through association. Try adding to #{reflection.through_reflection.name.inspect}.") + else + super("Read-only reflection error.") + end end end @@ -101,8 +191,12 @@ module ActiveRecord # (has_many, has_one) when there is at least 1 child associated instance. # ex: if @project.tasks.size > 0, DeleteRestrictionError will be raised when trying to destroy @project class DeleteRestrictionError < ActiveRecordError #:nodoc: - def initialize(name) - super("Cannot delete record because of dependent #{name}") + def initialize(name = nil) + if name + super("Cannot delete record because of dependent #{name}") + else + super("Delete restriction error.") + end end end @@ -128,14 +222,14 @@ module ActiveRecord autoload :ThroughAssociation module Builder #:nodoc: - autoload :Association, 'active_record/associations/builder/association' - autoload :SingularAssociation, 'active_record/associations/builder/singular_association' - autoload :CollectionAssociation, 'active_record/associations/builder/collection_association' + autoload :Association, "active_record/associations/builder/association" + autoload :SingularAssociation, "active_record/associations/builder/singular_association" + autoload :CollectionAssociation, "active_record/associations/builder/collection_association" - autoload :BelongsTo, 'active_record/associations/builder/belongs_to' - autoload :HasOne, 'active_record/associations/builder/has_one' - autoload :HasMany, 'active_record/associations/builder/has_many' - autoload :HasAndBelongsToMany, 'active_record/associations/builder/has_and_belongs_to_many' + autoload :BelongsTo, "active_record/associations/builder/belongs_to" + autoload :HasOne, "active_record/associations/builder/has_one" + autoload :HasMany, "active_record/associations/builder/has_many" + autoload :HasAndBelongsToMany, "active_record/associations/builder/has_and_belongs_to_many" end eager_autoload do @@ -145,6 +239,11 @@ module ActiveRecord autoload :AliasTracker end + def self.eager_load! + super + Preloader.eager_load! + end + # Returns the association instance for the given name, instantiating it if it doesn't already exist def association(name) #:nodoc: association = association_instance_get(name) @@ -176,16 +275,16 @@ module ActiveRecord private # Clears out the association cache. - def clear_association_cache # :nodoc: + def clear_association_cache @association_cache.clear if persisted? end - def init_internals # :nodoc: + def init_internals @association_cache = {} super end - # Returns the specified association instance if it exists, nil otherwise. + # Returns the specified association instance if it exists, +nil+ otherwise. def association_instance_get(name) @association_cache[name] end @@ -195,1550 +294,1561 @@ module ActiveRecord @association_cache[name] = association end - # \Associations are a set of macro-like class methods for tying objects together through - # foreign keys. They express relationships like "Project has one Project Manager" - # or "Project belongs to a Portfolio". Each macro adds a number of methods to the - # class which are specialized according to the collection or association symbol and the - # options hash. It works much the same way as Ruby's own <tt>attr*</tt> - # methods. - # - # class Project < ActiveRecord::Base - # belongs_to :portfolio - # has_one :project_manager - # has_many :milestones - # has_and_belongs_to_many :categories - # end - # - # The project class now has the following methods (and more) to ease the traversal and - # manipulation of its relationships: - # * <tt>Project#portfolio, Project#portfolio=(portfolio), Project#portfolio.nil?</tt> - # * <tt>Project#project_manager, Project#project_manager=(project_manager), Project#project_manager.nil?,</tt> - # * <tt>Project#milestones.empty?, Project#milestones.size, Project#milestones, Project#milestones<<(milestone),</tt> - # <tt>Project#milestones.delete(milestone), Project#milestones.destroy(milestone), Project#milestones.find(milestone_id),</tt> - # <tt>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), Project#categories.destroy(category1)</tt> - # - # === A word of warning - # - # Don't create associations that have the same name as instance methods of - # <tt>ActiveRecord::Base</tt>. Since the association adds a method with that name to - # its model, it will override the inherited method and break things. - # For instance, +attributes+ and +connection+ would be bad choices for association names. - # - # == Auto-generated methods - # See also Instance Public methods below for more details. - # - # === Singular associations (one-to-one) - # | | belongs_to | - # generated methods | belongs_to | :polymorphic | has_one - # ----------------------------------+------------+--------------+--------- - # other(force_reload=false) | X | X | X - # other=(other) | X | X | X - # build_other(attributes={}) | X | | X - # create_other(attributes={}) | X | | X - # create_other!(attributes={}) | X | | X - # - # ===Collection associations (one-to-many / many-to-many) - # | | | has_many - # generated methods | habtm | has_many | :through - # ----------------------------------+-------+----------+---------- - # others(force_reload=false) | X | X | X - # others=(other,other,...) | X | X | X - # other_ids | X | X | X - # other_ids=(id,id,...) | X | X | X - # others<< | X | X | X - # others.push | X | X | X - # others.concat | X | X | X - # others.build(attributes={}) | X | X | X - # others.create(attributes={}) | X | X | X - # others.create!(attributes={}) | X | X | X - # others.size | X | X | X - # others.length | X | X | X - # others.count | X | X | X - # others.sum(*args) | X | X | X - # others.empty? | X | X | X - # 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 - # others.distinct | X | X | X - # others.reset | X | X | X - # - # === Overriding generated methods - # - # Association methods are generated in a module that is included into the model class, - # which allows you to easily override with your own methods and call the original - # generated method with +super+. For example: - # - # class Car < ActiveRecord::Base - # belongs_to :owner - # belongs_to :old_owner - # def owner=(new_owner) - # self.old_owner = self.owner - # super - # end - # end - # - # If your model class is <tt>Project</tt>, the module is - # named <tt>Project::GeneratedAssociationMethods</tt>. The GeneratedAssociationMethods module is - # included in the model class immediately after the (anonymous) generated attributes methods - # module, meaning an association will override the methods for an attribute with the same name. - # - # == Cardinality and associations - # - # Active Record associations can be used to describe one-to-one, one-to-many and many-to-many - # relationships between models. Each model uses an association to describe its role in - # the relation. The +belongs_to+ association is always used in the model that has - # the foreign key. - # - # === One-to-one - # - # Use +has_one+ in the base, and +belongs_to+ in the associated model. - # - # class Employee < ActiveRecord::Base - # has_one :office - # end - # class Office < ActiveRecord::Base - # belongs_to :employee # foreign key - employee_id - # end - # - # === One-to-many - # - # Use +has_many+ in the base, and +belongs_to+ in the associated model. - # - # class Manager < ActiveRecord::Base - # has_many :employees - # end - # class Employee < ActiveRecord::Base - # belongs_to :manager # foreign key - manager_id - # end - # - # === Many-to-many - # - # There are two ways to build a many-to-many relationship. - # - # The first way uses a +has_many+ association with the <tt>:through</tt> option and a join model, so - # there are two stages of associations. - # - # class Assignment < ActiveRecord::Base - # belongs_to :programmer # foreign key - programmer_id - # belongs_to :project # foreign key - project_id - # end - # class Programmer < ActiveRecord::Base - # has_many :assignments - # has_many :projects, through: :assignments - # end - # class Project < ActiveRecord::Base - # has_many :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 - # that has no corresponding model or primary key. - # - # class Programmer < ActiveRecord::Base - # has_and_belongs_to_many :projects # foreign keys in the join table - # end - # class Project < ActiveRecord::Base - # has_and_belongs_to_many :programmers # foreign keys in the join table - # end - # - # Choosing which way to build a many-to-many relationship is not always simple. - # If you need to work with the relationship model as its own entity, - # use <tt>has_many :through</tt>. Use +has_and_belongs_to_many+ when working with legacy schemas or when - # you never work directly with the relationship itself. - # - # == Is it a +belongs_to+ or +has_one+ association? - # - # Both express a 1-1 relationship. The difference is mostly where to place the foreign - # key, which goes on the table for the class declaring the +belongs_to+ relationship. - # - # class User < ActiveRecord::Base - # # I reference an account. - # belongs_to :account - # end - # - # class Account < ActiveRecord::Base - # # One user references me. - # has_one :user - # end - # - # The tables for these classes could look something like: - # - # CREATE TABLE users ( - # id int(11) NOT NULL auto_increment, - # account_id int(11) default NULL, - # name varchar default NULL, - # PRIMARY KEY (id) - # ) - # - # CREATE TABLE accounts ( - # id int(11) NOT NULL auto_increment, - # name varchar default NULL, - # PRIMARY KEY (id) - # ) - # - # == Unsaved objects and associations - # - # You can manipulate objects and associations before they are saved to the database, but - # there is some special behavior you should be aware of, mostly involving the saving of - # associated objects. - # - # You can set the <tt>:autosave</tt> option on a <tt>has_one</tt>, <tt>belongs_to</tt>, - # <tt>has_many</tt>, or <tt>has_and_belongs_to_many</tt> association. Setting it - # to +true+ will _always_ save the members, whereas setting it to +false+ will - # _never_ save the members. More details about <tt>:autosave</tt> option is available at - # AutosaveAssociation. - # - # === One-to-one associations - # - # * Assigning an object to a +has_one+ association automatically saves that object and - # the object being replaced (if there is one), in order to update their foreign - # keys - except if the parent object is unsaved (<tt>new_record? == true</tt>). - # * If either of these saves fail (due to one of the objects being invalid), an - # <tt>ActiveRecord::RecordNotSaved</tt> exception is raised and the assignment is - # cancelled. - # * If you wish to assign an object to a +has_one+ association without saving it, - # use the <tt>build_association</tt> method (documented below). The object being - # replaced will still be saved to update its foreign key. - # * Assigning an object to a +belongs_to+ association does not save the object, since - # the foreign key field belongs on the parent. It does not save the parent either. - # - # === Collections - # - # * Adding an object to a collection (+has_many+ or +has_and_belongs_to_many+) automatically - # saves that object, except if the parent object (the owner of the collection) is not yet - # stored in the database. - # * If saving any of the objects being added to a collection (via <tt>push</tt> or similar) - # fails, then <tt>push</tt> returns +false+. - # * If saving fails while replacing the collection (via <tt>association=</tt>), an - # <tt>ActiveRecord::RecordNotSaved</tt> exception is raised and the assignment is - # cancelled. - # * You can add an object to a collection without automatically saving it by using the - # <tt>collection.build</tt> method (documented below). - # * All unsaved (<tt>new_record? == true</tt>) members of the collection are automatically - # saved when the parent is saved. - # - # == Customizing the query - # - # \Associations are built from <tt>Relation</tt>s, and you can use the <tt>Relation</tt> syntax - # to customize them. For example, to add a condition: - # - # class Blog < ActiveRecord::Base - # has_many :published_posts, -> { where published: true }, class_name: 'Post' - # end - # - # Inside the <tt>-> { ... }</tt> block you can use all of the usual <tt>Relation</tt> methods. - # - # === Accessing the owner object - # - # Sometimes it is useful to have access to the owner object when building the query. The owner - # is passed as a parameter to the block. For example, the following association would find all - # events that occur on the user's birthday: - # - # class User < ActiveRecord::Base - # has_many :birthday_events, ->(user) { where starts_on: user.birthday }, class_name: 'Event' - # end - # - # Note: Joining, eager loading and preloading of these associations is not fully possible. - # These operations happen before instance creation and the scope will be called with a +nil+ argument. - # This can lead to unexpected behavior and is deprecated. - # - # == Association callbacks - # - # Similar to the normal callbacks that hook into the life cycle of an Active Record object, - # you can also define callbacks that get triggered when you add an object to or remove an - # object from an association collection. - # - # class Project - # has_and_belongs_to_many :developers, after_add: :evaluate_velocity - # - # def evaluate_velocity(developer) - # ... - # end - # end - # - # It's possible to stack callbacks by passing them as an array. Example: - # - # class Project - # has_and_belongs_to_many :developers, - # after_add: [:evaluate_velocity, Proc.new { |p, d| p.shipping_date = Time.now}] - # end - # - # Possible callbacks are: +before_add+, +after_add+, +before_remove+ and +after_remove+. - # - # If any of the +before_add+ callbacks throw an exception, the object will not be - # added to the collection. - # - # Similarly, if any of the +before_remove+ callbacks throw an exception, the object - # will not be removed from the collection. - # - # == Association extensions - # - # The proxy objects that control the access to associations can be extended through anonymous - # modules. This is especially beneficial for adding new finders, creators, and other - # factory-type methods that are only used as part of this association. - # - # class Account < ActiveRecord::Base - # has_many :people do - # def find_or_create_by_name(name) - # first_name, last_name = name.split(" ", 2) - # find_or_create_by(first_name: first_name, last_name: last_name) - # end - # end - # end - # - # person = Account.first.people.find_or_create_by_name("David Heinemeier Hansson") - # person.first_name # => "David" - # person.last_name # => "Heinemeier Hansson" - # - # If you need to share the same extensions between many associations, you can use a named - # extension module. - # - # module FindOrCreateByNameExtension - # def find_or_create_by_name(name) - # first_name, last_name = name.split(" ", 2) - # find_or_create_by(first_name: first_name, last_name: last_name) - # end - # end - # - # class Account < ActiveRecord::Base - # has_many :people, -> { extending FindOrCreateByNameExtension } - # end - # - # class Company < ActiveRecord::Base - # has_many :people, -> { extending FindOrCreateByNameExtension } - # end - # - # Some extensions can only be made to work with knowledge of the association's internals. - # Extensions can access relevant state using the following methods (where +items+ is the - # name of the association): - # - # * <tt>record.association(:items).owner</tt> - Returns the object the association is part of. - # * <tt>record.association(:items).reflection</tt> - Returns the reflection object that describes the association. - # * <tt>record.association(:items).target</tt> - Returns the associated object for +belongs_to+ and +has_one+, or - # the collection of associated objects for +has_many+ and +has_and_belongs_to_many+. - # - # However, inside the actual extension code, you will not have access to the <tt>record</tt> as - # above. In this case, you can access <tt>proxy_association</tt>. For example, - # <tt>record.association(:items)</tt> and <tt>record.items.proxy_association</tt> will return - # the same object, allowing you to make calls like <tt>proxy_association.owner</tt> inside - # association extensions. - # - # == Association Join Models - # - # Has Many associations can be configured with the <tt>:through</tt> option to use an - # explicit join model to retrieve the data. This operates similarly to a - # +has_and_belongs_to_many+ association. The advantage is that you're able to add validations, - # callbacks, and extra attributes on the join model. Consider the following schema: - # - # class Author < ActiveRecord::Base - # has_many :authorships - # has_many :books, through: :authorships - # end - # - # class Authorship < ActiveRecord::Base - # belongs_to :author - # belongs_to :book - # end - # - # @author = Author.first - # @author.authorships.collect { |a| a.book } # selects all books that the author's authorships belong to - # @author.books # selects all books by using the Authorship join model - # - # You can also go through a +has_many+ association on the join model: - # - # class Firm < ActiveRecord::Base - # has_many :clients - # has_many :invoices, through: :clients - # end - # - # class Client < ActiveRecord::Base - # belongs_to :firm - # has_many :invoices - # end - # - # class Invoice < ActiveRecord::Base - # belongs_to :client - # end - # - # @firm = Firm.first - # @firm.clients.flat_map { |c| c.invoices } # select all invoices for all clients of the firm - # @firm.invoices # selects all invoices by going through the Client join model - # - # Similarly you can go through a +has_one+ association on the join model: - # - # class Group < ActiveRecord::Base - # has_many :users - # has_many :avatars, through: :users - # end - # - # class User < ActiveRecord::Base - # belongs_to :group - # has_one :avatar - # end - # - # class Avatar < ActiveRecord::Base - # belongs_to :user - # end - # - # @group = Group.first - # @group.users.collect { |u| u.avatar }.compact # select all avatars for all users in the group - # @group.avatars # selects all avatars by going through the User join model. - # - # An important caveat with going through +has_one+ or +has_many+ associations on the - # join model is that these associations are *read-only*. For example, the following - # would not work following the previous example: - # - # @group.avatars << Avatar.new # this would work if User belonged_to Avatar rather than the other way around - # @group.avatars.delete(@group.avatars.last) # so would this - # - # == Setting Inverses - # - # If you are using a +belongs_to+ on the join model, it is a good idea to set the - # <tt>:inverse_of</tt> option on the +belongs_to+, which will mean that the following example - # works correctly (where <tt>tags</tt> is a +has_many+ <tt>:through</tt> association): - # - # @post = Post.first - # @tag = @post.tags.build name: "ruby" - # @tag.save - # - # The last line ought to save the through record (a <tt>Taggable</tt>). This will only work if the - # <tt>:inverse_of</tt> is set: - # - # class Taggable < ActiveRecord::Base - # belongs_to :post - # belongs_to :tag, inverse_of: :taggings - # end - # - # If you do not set the <tt>:inverse_of</tt> record, the association will - # do its best to match itself up with the correct inverse. Automatic - # inverse detection only works on <tt>has_many</tt>, <tt>has_one</tt>, and - # <tt>belongs_to</tt> associations. - # - # Extra options on the associations, as defined in the - # <tt>AssociationReflection::INVALID_AUTOMATIC_INVERSE_OPTIONS</tt> constant, will - # also prevent the association's inverse from being found automatically. - # - # The automatic guessing of the inverse association uses a heuristic based - # on the name of the class, so it may not work for all associations, - # especially the ones with non-standard names. - # - # You can turn off the automatic detection of inverse associations by setting - # the <tt>:inverse_of</tt> option to <tt>false</tt> like so: - # - # class Taggable < ActiveRecord::Base - # belongs_to :tag, inverse_of: false - # end - # - # == Nested \Associations - # - # You can actually specify *any* association with the <tt>:through</tt> option, including an - # association which has a <tt>:through</tt> option itself. For example: - # - # class Author < ActiveRecord::Base - # has_many :posts - # has_many :comments, through: :posts - # has_many :commenters, through: :comments - # end - # - # class Post < ActiveRecord::Base - # has_many :comments - # end - # - # class Comment < ActiveRecord::Base - # belongs_to :commenter - # end - # - # @author = Author.first - # @author.commenters # => People who commented on posts written by the author - # - # An equivalent way of setting up this association this would be: - # - # class Author < ActiveRecord::Base - # has_many :posts - # has_many :commenters, through: :posts - # end - # - # class Post < ActiveRecord::Base - # has_many :comments - # has_many :commenters, through: :comments - # end - # - # class Comment < ActiveRecord::Base - # belongs_to :commenter - # end - # - # When using a nested association, you will not be able to modify the association because there - # is not enough information to know what modification to make. For example, if you tried to - # add a <tt>Commenter</tt> in the example above, there would be no way to tell how to set up the - # intermediate <tt>Post</tt> and <tt>Comment</tt> objects. - # - # == Polymorphic \Associations - # - # Polymorphic associations on models are not restricted on what types of models they - # can be associated with. Rather, they specify an interface that a +has_many+ association - # must adhere to. - # - # class Asset < ActiveRecord::Base - # belongs_to :attachable, polymorphic: true - # end - # - # class Post < ActiveRecord::Base - # has_many :assets, as: :attachable # The :as option specifies the polymorphic interface to use. - # end - # - # @asset.attachable = @post - # - # This works by using a type column in addition to a foreign key to specify the associated - # record. In the Asset example, you'd need an +attachable_id+ integer column and an - # +attachable_type+ string column. - # - # Using polymorphic associations in combination with single table inheritance (STI) is - # a little tricky. In order for the associations to work as expected, ensure that you - # store the base model for the STI models in the type column of the polymorphic - # association. To continue with the asset example above, suppose there are guest posts - # and member posts that use the posts table for STI. In this case, there must be a +type+ - # column in the posts table. - # - # Note: The <tt>attachable_type=</tt> method is being called when assigning an +attachable+. - # The +class_name+ of the +attachable+ is passed as a String. - # - # class Asset < ActiveRecord::Base - # belongs_to :attachable, polymorphic: true - # - # def attachable_type=(class_name) - # super(class_name.constantize.base_class.to_s) - # end - # end - # - # class Post < ActiveRecord::Base - # # because we store "Post" in attachable_type now dependent: :destroy will work - # has_many :assets, as: :attachable, dependent: :destroy - # end - # - # class GuestPost < Post - # end - # - # class MemberPost < Post - # end - # - # == Caching - # - # All of the methods are built on a simple caching principle that will keep the result - # of the last query around unless specifically instructed not to. The cache is even - # shared across methods to make it even cheaper to use the macro-added methods without - # worrying too much about performance at the first go. - # - # project.milestones # fetches milestones from the database - # project.milestones.size # uses the milestone cache - # project.milestones.empty? # uses the milestone cache - # project.milestones(true).size # fetches milestones from the database - # project.milestones # uses the milestone cache - # - # == Eager loading of associations - # - # Eager loading is a way to find objects of a certain class and a number of named associations. - # It is one of the easiest ways to prevent the dreaded N+1 problem in which fetching 100 - # posts that each need to display their author triggers 101 database queries. Through the - # use of eager loading, the number of queries will be reduced from 101 to 2. - # - # class Post < ActiveRecord::Base - # belongs_to :author - # has_many :comments - # end - # - # Consider the following loop using the class above: - # - # Post.all.each do |post| - # puts "Post: " + post.title - # puts "Written by: " + post.author.name - # puts "Last comment on: " + post.comments.first.created_on - # end - # - # To iterate over these one hundred posts, we'll generate 201 database queries. Let's - # first just optimize it for retrieving the author: - # - # Post.includes(:author).each do |post| - # - # This references the name of the +belongs_to+ association that also used the <tt>:author</tt> - # symbol. After loading the posts, find will collect the +author_id+ from each one and load - # all the referenced authors with one query. Doing so will cut down the number of queries - # from 201 to 102. - # - # We can improve upon the situation further by referencing both associations in the finder with: - # - # Post.includes(:author, :comments).each do |post| - # - # This will load all comments with a single query. This reduces the total number of queries - # to 3. In general, the number of queries will be 1 plus the number of associations - # named (except if some of the associations are polymorphic +belongs_to+ - see below). - # - # To include a deep hierarchy of associations, use a hash: - # - # Post.includes(:author, { comments: { author: :gravatar } }).each do |post| - # - # The above code will load all the comments and all of their associated - # authors and gravatars. You can mix and match any combination of symbols, - # arrays, and hashes to retrieve the associations you want to load. - # - # All of this power shouldn't fool you into thinking that you can pull out huge amounts - # of data with no performance penalty just because you've reduced the number of queries. - # The database still needs to send all the data to Active Record and it still needs to - # be processed. So it's no catch-all for performance problems, but it's a great way to - # cut down on the number of queries in a situation as the one described above. - # - # Since only one table is loaded at a time, conditions or orders cannot reference tables - # other than the main one. If this is the case, Active Record falls back to the previously - # used LEFT OUTER JOIN based strategy. For example: - # - # Post.includes([:author, :comments]).where(['comments.approved = ?', true]) - # - # This will result in a single SQL query with joins along the lines of: - # <tt>LEFT OUTER JOIN comments ON comments.post_id = posts.id</tt> and - # <tt>LEFT OUTER JOIN authors ON authors.id = posts.author_id</tt>. Note that using conditions - # like this can have unintended consequences. - # In the above example posts with no approved comments are not returned at all, because - # the conditions apply to the SQL statement as a whole and not just to the association. - # - # You must disambiguate column references for this fallback to happen, for example - # <tt>order: "author.name DESC"</tt> will work but <tt>order: "name DESC"</tt> will not. - # - # If you want to load all posts (including posts with no approved comments) then write - # your own LEFT OUTER JOIN query using ON - # - # Post.joins("LEFT OUTER JOIN comments ON comments.post_id = posts.id AND comments.approved = '1'") - # - # In this case it is usually more natural to include an association which has conditions defined on it: - # - # class Post < ActiveRecord::Base - # has_many :approved_comments, -> { where approved: true }, class_name: 'Comment' - # end - # - # Post.includes(:approved_comments) - # - # This will load posts and eager load the +approved_comments+ association, which contains - # only those comments that have been approved. - # - # If you eager load an association with a specified <tt>:limit</tt> option, it will be ignored, - # returning all the associated objects: - # - # class Picture < ActiveRecord::Base - # 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. - # - # Eager loading is supported with polymorphic associations. - # - # class Address < ActiveRecord::Base - # belongs_to :addressable, polymorphic: true - # end - # - # A call that tries to eager load the addressable model - # - # Address.includes(:addressable) - # - # This will execute one query to load the addresses and load the addressables with one - # query per addressable type. - # For example if all the addressables are either of class Person or Company then a total - # of 3 queries will be executed. The list of addressable types to load is determined on - # the back of the addresses loaded. This is not supported if Active Record has to fallback - # to the previous implementation of eager loading and will raise <tt>ActiveRecord::EagerLoadPolymorphicError</tt>. - # The reason is that the parent model's type is a column value so its corresponding table - # name cannot be put in the +FROM+/+JOIN+ clauses of that query. - # - # == Table Aliasing - # - # Active Record uses table aliasing in the case that a table is referenced multiple times - # in a join. If a table is referenced only once, the standard table name is used. The - # second time, the table is aliased as <tt>#{reflection_name}_#{parent_table_name}</tt>. - # Indexes are appended for any more successive uses of the table name. - # - # Post.joins(:comments) - # # => SELECT ... FROM posts INNER JOIN comments ON ... - # Post.joins(:special_comments) # STI - # # => SELECT ... FROM posts INNER JOIN comments ON ... AND comments.type = 'SpecialComment' - # Post.joins(:comments, :special_comments) # special_comments is the reflection name, posts is the parent table name - # # => SELECT ... FROM posts INNER JOIN comments ON ... INNER JOIN comments special_comments_posts - # - # Acts as tree example: - # - # TreeMixin.joins(:children) - # # => SELECT ... FROM mixins INNER JOIN mixins childrens_mixins ... - # TreeMixin.joins(children: :parent) - # # => SELECT ... FROM mixins INNER JOIN mixins childrens_mixins ... - # INNER JOIN parents_mixins ... - # TreeMixin.joins(children: {parent: :children}) - # # => SELECT ... FROM mixins INNER JOIN mixins childrens_mixins ... - # INNER JOIN parents_mixins ... - # INNER JOIN mixins childrens_mixins_2 - # - # Has and Belongs to Many join tables use the same idea, but add a <tt>_join</tt> suffix: - # - # Post.joins(:categories) - # # => SELECT ... FROM posts INNER JOIN categories_posts ... INNER JOIN categories ... - # 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}) - # # => 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 - # - # If you wish to specify your own custom joins using <tt>joins</tt> method, those table - # names will take precedence over the eager associations: - # - # Post.joins(:comments).joins("inner join comments ...") - # # => SELECT ... FROM posts INNER JOIN comments_posts ON ... INNER JOIN comments ... - # Post.joins(:comments, :special_comments).joins("inner join comments ...") - # # => SELECT ... FROM posts INNER JOIN comments comments_posts ON ... - # INNER JOIN comments special_comments_posts ... - # INNER JOIN comments ... - # - # Table aliases are automatically truncated according to the maximum length of table identifiers - # according to the specific database. - # - # == Modules - # - # By default, associations will look for objects within the current module scope. Consider: - # - # module MyApplication - # module Business - # class Firm < ActiveRecord::Base - # has_many :clients - # end - # - # class Client < ActiveRecord::Base; end - # end - # end - # - # When <tt>Firm#clients</tt> is called, it will in turn call - # <tt>MyApplication::Business::Client.find_all_by_firm_id(firm.id)</tt>. - # If you want to associate with a class in another module scope, this can be done by - # specifying the complete class name. - # - # module MyApplication - # module Business - # class Firm < ActiveRecord::Base; end - # end - # - # module Billing - # class Account < ActiveRecord::Base - # belongs_to :firm, class_name: "MyApplication::Business::Firm" - # end - # end - # end - # - # == Bi-directional associations - # - # When you specify an association there is usually an association on the associated model - # that specifies the same relationship in reverse. For example, with the following models: - # - # class Dungeon < ActiveRecord::Base - # has_many :traps - # has_one :evil_wizard - # end - # - # class Trap < ActiveRecord::Base - # belongs_to :dungeon - # end - # - # class EvilWizard < ActiveRecord::Base - # belongs_to :dungeon - # end - # - # The +traps+ association on +Dungeon+ and the +dungeon+ association on +Trap+ are - # the inverse of each other and the inverse of the +dungeon+ association on +EvilWizard+ - # is the +evil_wizard+ association on +Dungeon+ (and vice-versa). By default, - # Active Record can guess the inverse of the association based on the name - # of the class. The result is the following: - # - # d = Dungeon.first - # t = d.traps.first - # d.object_id == t.dungeon.object_id # => true - # - # The +Dungeon+ instances +d+ and <tt>t.dungeon</tt> in the above example refer to - # the same in-memory instance since the association matches the name of the class. - # The result would be the same if we added +:inverse_of+ to our model definitions: - # - # class Dungeon < ActiveRecord::Base - # has_many :traps, inverse_of: :dungeon - # has_one :evil_wizard, inverse_of: :dungeon - # end - # - # class Trap < ActiveRecord::Base - # belongs_to :dungeon, inverse_of: :traps - # end - # - # class EvilWizard < ActiveRecord::Base - # belongs_to :dungeon, inverse_of: :evil_wizard - # end - # - # There are limitations to <tt>:inverse_of</tt> support: - # - # * does not work with <tt>:through</tt> associations. - # * does not work with <tt>:polymorphic</tt> associations. - # * for +belongs_to+ associations +has_many+ inverse associations are ignored. - # - # For more information, see the documentation for the +:inverse_of+ option. - # - # == Deleting from associations - # - # === Dependent associations - # - # +has_many+, +has_one+ and +belongs_to+ associations support the <tt>:dependent</tt> option. - # This allows you to specify that associated records should be deleted when the owner is - # deleted. - # - # For example: - # - # class Author - # has_many :posts, dependent: :destroy - # end - # Author.find(1).destroy # => Will destroy all of the author's posts, too - # - # The <tt>:dependent</tt> option can have different values which specify how the deletion - # is done. For more information, see the documentation for this option on the different - # specific association types. When no option is given, the behavior 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. - # - # Note that <tt>:dependent</tt> option is ignored for +has_one+ <tt>:through</tt> associations. - # - # === Delete or destroy? - # - # +has_many+ and +has_and_belongs_to_many+ associations have the methods <tt>destroy</tt>, - # <tt>delete</tt>, <tt>destroy_all</tt> and <tt>delete_all</tt>. - # - # For +has_and_belongs_to_many+, <tt>delete</tt> and <tt>destroy</tt> are the same: they - # cause the records in the join table to be removed. - # - # For +has_many+, <tt>destroy</tt> and <tt>destroy_all</tt> will always call the <tt>destroy</tt> method of the - # record(s) being removed so that callbacks are run. However <tt>delete</tt> and <tt>delete_all</tt> will either - # do the deletion according to the strategy specified by the <tt>:dependent</tt> option, or - # if no <tt>:dependent</tt> option is given, then it will follow the default strategy. - # The default strategy is to do nothing (leave the foreign keys with the parent ids set), except for - # +has_many+ <tt>:through</tt>, where the default strategy is <tt>delete_all</tt> (delete - # the join records, without running their callbacks). - # - # There is also a <tt>clear</tt> method which is the same as <tt>delete_all</tt>, except that - # it returns the association rather than the records which have been deleted. - # - # === What gets deleted? - # - # There is a potential pitfall here: +has_and_belongs_to_many+ and +has_many+ <tt>:through</tt> - # associations have records in join tables, as well as the associated records. So when we - # call one of these deletion methods, what exactly should be deleted? - # - # The answer is that it is assumed that deletion on an association is about removing the - # <i>link</i> between the owner and the associated object(s), rather than necessarily the - # associated objects themselves. So with +has_and_belongs_to_many+ and +has_many+ - # <tt>:through</tt>, the join records will be deleted, but the associated records won't. - # - # This makes sense if you think about it: if you were to call <tt>post.tags.delete(Tag.find_by(name: 'food'))</tt> - # you would want the 'food' tag to be unlinked from the post, rather than for the tag itself - # to be removed from the database. - # - # However, there are examples where this strategy doesn't make sense. For example, suppose - # a person has many projects, and each project has many tasks. If we deleted one of a person's - # tasks, we would probably not want the project to be deleted. In this scenario, the delete method - # won't actually work: it can only be used if the association on the join model is a - # +belongs_to+. In other situations you are expected to perform operations directly on - # either the associated records or the <tt>:through</tt> association. - # - # With a regular +has_many+ there is no distinction between the "associated records" - # and the "link", so there is only one choice for what gets deleted. - # - # With +has_and_belongs_to_many+ and +has_many+ <tt>:through</tt>, if you want to delete the - # associated records themselves, you can always do something along the lines of - # <tt>person.tasks.each(&:destroy)</tt>. - # - # == Type safety with <tt>ActiveRecord::AssociationTypeMismatch</tt> - # - # If you attempt to assign an object to an association that doesn't match the inferred - # or specified <tt>:class_name</tt>, you'll get an <tt>ActiveRecord::AssociationTypeMismatch</tt>. - # - # == Options - # - # All of the association macros can be specialized through options. This makes cases - # more complex than the simple and guessable ones possible. - module ClassMethods - # Specifies a one-to-many association. The following methods for retrieval and query of - # collections of associated objects will be added: - # - # +collection+ is a placeholder for the symbol passed as the +name+ argument, so - # <tt>has_many :clients</tt> would add among others <tt>clients.empty?</tt>. - # - # [collection(force_reload = false)] - # Returns an array of all the associated objects. - # An empty array is returned if none are found. - # [collection<<(object, ...)] - # Adds one or more objects to the collection by setting their foreign keys to the collection's primary key. - # Note that this operation instantly fires update SQL without waiting for the save or update call on the - # parent object, unless the parent object is a new record. - # [collection.delete(object, ...)] - # Removes one or more objects from the collection by setting their foreign keys to +NULL+. - # Objects will be in addition destroyed if they're associated with <tt>dependent: :destroy</tt>, - # and deleted if they're associated with <tt>dependent: :delete_all</tt>. - # - # If the <tt>:through</tt> option is used, then the join records are deleted (rather than - # nullified) by default, but you can specify <tt>dependent: :destroy</tt> or - # <tt>dependent: :nullify</tt> to override this. - # [collection.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 - # direct. - # [collection_singular_ids] - # Returns an array of the associated objects' ids - # [collection_singular_ids=ids] - # Replace the collection with the objects identified by the primary keys in +ids+. This - # 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+. - # If the <tt>:through</tt> option is true no destroy callbacks are invoked on the join models. - # Join models are directly deleted. - # [collection.empty?] - # Returns +true+ if there are no associated objects. - # [collection.size] - # Returns the number of associated objects. - # [collection.find(...)] - # Finds an associated object according to the same rules as <tt>ActiveRecord::Base.find</tt>. - # [collection.exists?(...)] - # Checks whether an associated object with the given conditions exists. - # Uses the same rules as <tt>ActiveRecord::Base.exists?</tt>. - # [collection.build(attributes = {}, ...)] - # Returns one or more new objects of the collection type that have been instantiated - # with +attributes+ and linked to this object through a foreign key, but have not yet - # been saved. - # [collection.create(attributes = {})] - # Returns a new object of the collection type that has been instantiated - # with +attributes+, linked to this object through a foreign key, and that has already - # been saved (if it passed the validation). *Note*: This only works if the base model - # already exists in the DB, not if it is a new (unsaved) record! - # [collection.create!(attributes = {})] - # Does the same as <tt>collection.create</tt>, but raises <tt>ActiveRecord::RecordInvalid</tt> - # if the record is invalid. - # - # === Example - # - # A <tt>Firm</tt> class declares <tt>has_many :clients</tt>, which will add: - # * <tt>Firm#clients</tt> (similar to <tt>Client.where(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.where(firm_id: id).find(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>) - # * <tt>Firm#clients.create!</tt> (similar to <tt>c = Client.new("firm_id" => id); c.save!</tt>) - # The declaration can also include an +options+ hash to specialize the behavior of the association. - # - # === Scopes - # - # You can pass a second argument +scope+ as a callable (i.e. proc or - # lambda) to retrieve a specific set of records or customize the generated - # query when you access the associated collection. - # - # Scope examples: - # has_many :comments, -> { where(author_id: 1) } - # has_many :employees, -> { joins(:address) } - # has_many :posts, ->(post) { where("max_post_length > ?", post.length) } - # - # === Extensions - # - # The +extension+ argument allows you to pass a block into a has_many - # association. This is useful for adding new finders, creators and other - # factory-type methods to be used as part of the association. - # - # Extension examples: - # has_many :employees do - # def find_or_create_by_name(name) - # first_name, last_name = name.split(" ", 2) - # find_or_create_by(first_name: first_name, last_name: last_name) + # \Associations are a set of macro-like class methods for tying objects together through + # foreign keys. They express relationships like "Project has one Project Manager" + # or "Project belongs to a Portfolio". Each macro adds a number of methods to the + # class which are specialized according to the collection or association symbol and the + # options hash. It works much the same way as Ruby's own <tt>attr*</tt> + # methods. + # + # class Project < ActiveRecord::Base + # belongs_to :portfolio + # has_one :project_manager + # has_many :milestones + # has_and_belongs_to_many :categories + # end + # + # The project class now has the following methods (and more) to ease the traversal and + # manipulation of its relationships: + # * <tt>Project#portfolio, Project#portfolio=(portfolio), Project#portfolio.nil?</tt> + # * <tt>Project#project_manager, Project#project_manager=(project_manager), Project#project_manager.nil?,</tt> + # * <tt>Project#milestones.empty?, Project#milestones.size, Project#milestones, Project#milestones<<(milestone),</tt> + # <tt>Project#milestones.delete(milestone), Project#milestones.destroy(milestone), Project#milestones.find(milestone_id),</tt> + # <tt>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), Project#categories.destroy(category1)</tt> + # + # === A word of warning + # + # Don't create associations that have the same name as {instance methods}[rdoc-ref:ActiveRecord::Core] of + # <tt>ActiveRecord::Base</tt>. Since the association adds a method with that name to + # its model, using an association with the same name as one provided by <tt>ActiveRecord::Base</tt> will override the method inherited through <tt>ActiveRecord::Base</tt> and will break things. + # For instance, +attributes+ and +connection+ would be bad choices for association names, because those names already exist in the list of <tt>ActiveRecord::Base</tt> instance methods. + # + # == Auto-generated methods + # See also Instance Public methods below for more details. + # + # === Singular associations (one-to-one) + # | | belongs_to | + # generated methods | belongs_to | :polymorphic | has_one + # ----------------------------------+------------+--------------+--------- + # other(force_reload=false) | X | X | X + # other=(other) | X | X | X + # build_other(attributes={}) | X | | X + # create_other(attributes={}) | X | | X + # create_other!(attributes={}) | X | | X + # + # === Collection associations (one-to-many / many-to-many) + # | | | has_many + # generated methods | habtm | has_many | :through + # ----------------------------------+-------+----------+---------- + # others(force_reload=false) | X | X | X + # others=(other,other,...) | X | X | X + # other_ids | X | X | X + # other_ids=(id,id,...) | X | X | X + # others<< | X | X | X + # others.push | X | X | X + # others.concat | X | X | X + # others.build(attributes={}) | X | X | X + # others.create(attributes={}) | X | X | X + # others.create!(attributes={}) | X | X | X + # others.size | X | X | X + # others.length | X | X | X + # others.count | X | X | X + # others.sum(*args) | X | X | X + # others.empty? | X | X | X + # 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 + # others.distinct | X | X | X + # others.reset | X | X | X + # + # === Overriding generated methods + # + # Association methods are generated in a module that is included into the model class, + # which allows you to easily override with your own methods and call the original + # generated method with +super+. For example: + # + # class Car < ActiveRecord::Base + # belongs_to :owner + # belongs_to :old_owner + # def owner=(new_owner) + # self.old_owner = self.owner + # super # end # end # - # === Options - # [:class_name] - # Specify the class name of the association. Use it only if that name can't be inferred - # from the association name. So <tt>has_many :products</tt> will by default be linked - # to the Product class, but if the real class name is SpecialProduct, you'll have to - # specify it with this option. - # [:foreign_key] - # Specify the foreign key used for the association. By default this is guessed to be the name - # of this class in lower-case and "_id" suffixed. So a Person class that makes a +has_many+ - # association will use "person_id" as the default <tt>:foreign_key</tt>. - # [:foreign_type] - # Specify the column used to store the associated object's type, if this is a polymorphic - # association. By default this is guessed to be the name of the polymorphic association - # specified on "as" option with a "_type" suffix. So a class that defines a - # <tt>has_many :tags, as: :taggable</tt> association will use "taggable_type" as the - # default <tt>:foreign_type</tt>. - # [:primary_key] - # Specify the name of the column to use as the primary key for the association. By default this is +id+. - # [:dependent] - # Controls what happens to the associated objects when - # their owner is destroyed. Note that these are implemented as - # callbacks, and Rails executes callbacks in order. Therefore, other - # similar callbacks may affect the <tt>:dependent</tt> behavior, and the - # <tt>:dependent</tt> behavior may affect other callbacks. - # - # * <tt>:destroy</tt> causes all the associated objects to also be destroyed. - # * <tt>:delete_all</tt> causes all the associated objects to be deleted directly from the database (so callbacks will not be executed). - # * <tt>:nullify</tt> causes the foreign keys to be set to +NULL+. Callbacks are not executed. - # * <tt>:restrict_with_exception</tt> causes an exception to be raised if there are any associated records. - # * <tt>:restrict_with_error</tt> causes an error to be added to the owner if there are any associated objects. - # - # If using with the <tt>:through</tt> option, the association on the join model must be - # a +belongs_to+, and the records which get deleted are the join records, rather than - # the associated records. - # [: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] - # Specifies an association through which to perform the query. This can be any other type - # of association, including other <tt>:through</tt> associations. Options for <tt>:class_name</tt>, - # <tt>:primary_key</tt> and <tt>:foreign_key</tt> are ignored, as the association uses the - # source reflection. - # - # If the association on the join model is a +belongs_to+, the collection can be modified - # and the records on the <tt>:through</tt> model will be automatically created and removed - # as appropriate. Otherwise, the collection is read-only, so you should manipulate the - # <tt>:through</tt> association directly. - # - # If you are going to modify the association (rather than just read from it), then it is - # a good idea to set the <tt>:inverse_of</tt> option on the source association on the - # join model. This allows associated records to be built which will automatically create - # the appropriate join model records when they are saved. (See the 'Association Join Models' - # section above.) - # [:source] - # Specifies the source association name used by <tt>has_many :through</tt> queries. - # Only use it if the name cannot be inferred from the association. - # <tt>has_many :subscribers, through: :subscriptions</tt> will look for either <tt>:subscribers</tt> or - # <tt>:subscriber</tt> on Subscription, unless a <tt>:source</tt> is given. - # [:source_type] - # Specifies type of the source association used by <tt>has_many :through</tt> queries where the source - # association is a polymorphic +belongs_to+. - # [:validate] - # If +false+, don't validate the associated objects when saving the parent object. true by default. - # [:autosave] - # If true, always save the associated objects or destroy them if marked for destruction, - # when saving the parent object. If false, never save or destroy the associated objects. - # By default, only save associated objects that are new records. This option is implemented as a - # +before_save+ callback. Because callbacks are run in the order they are defined, associated objects - # may need to be explicitly saved in any user-defined +before_save+ callbacks. - # - # Note that <tt>accepts_nested_attributes_for</tt> sets <tt>:autosave</tt> to <tt>true</tt>. - # [:inverse_of] - # Specifies the name of the <tt>belongs_to</tt> association on the associated object - # that is the inverse of this <tt>has_many</tt> association. Does not work in combination - # with <tt>:through</tt> or <tt>:as</tt> options. - # See ActiveRecord::Associations::ClassMethods's overview on Bi-directional associations for more detail. - # [:extend] - # Specifies a module or array of modules that will be extended into the association object returned. - # Useful for defining methods on associations, especially when they should be shared between multiple - # association objects. - # - # Option examples: - # has_many :comments, -> { order "posted_on" } - # has_many :comments, -> { includes :author } - # has_many :people, -> { where(deleted: false).order("name") }, class_name: "Person" - # has_many :tracks, -> { order "position" }, dependent: :destroy - # has_many :comments, dependent: :nullify - # has_many :tags, as: :taggable - # has_many :reports, -> { readonly } - # has_many :subscribers, through: :subscriptions, source: :user - def has_many(name, scope = nil, options = {}, &extension) - reflection = Builder::HasMany.build(self, name, scope, options, &extension) - Reflection.add_reflection self, name, reflection - end - - # Specifies a one-to-one association with another class. This method should only be used - # if the other class contains the foreign key. If the current class contains the foreign key, - # then you should use +belongs_to+ instead. See also ActiveRecord::Associations::ClassMethods's overview - # on when to use +has_one+ and when to use +belongs_to+. - # - # The following methods for retrieval and query of a single associated object will be added: - # - # +association+ is a placeholder for the symbol passed as the +name+ argument, so - # <tt>has_one :manager</tt> would add among others <tt>manager.nil?</tt>. - # - # [association(force_reload = false)] - # Returns the associated object. +nil+ is returned if none is found. - # [association=(associate)] - # Assigns the associate object, extracts the primary key, sets it as the foreign key, - # and saves the associate object. To avoid database inconsistencies, permanently deletes an existing - # associated object when assigning a new one, even if the new one isn't saved to database. - # [build_association(attributes = {})] - # Returns a new object of the associated type that has been instantiated - # with +attributes+ and linked to this object through a foreign key, but has not - # yet been saved. - # [create_association(attributes = {})] - # Returns a new object of the associated type that has been instantiated - # with +attributes+, linked to this object through a foreign key, and that - # has already been saved (if it passed the validation). - # [create_association!(attributes = {})] - # Does the same as <tt>create_association</tt>, but raises <tt>ActiveRecord::RecordInvalid</tt> - # if the record is invalid. - # - # === Example - # - # An Account class declares <tt>has_one :beneficiary</tt>, which will add: - # * <tt>Account#beneficiary</tt> (similar to <tt>Beneficiary.where(account_id: id).first</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>) - # * <tt>Account#create_beneficiary!</tt> (similar to <tt>b = Beneficiary.new("account_id" => id); b.save!; b</tt>) - # - # === Scopes - # - # You can pass a second argument +scope+ as a callable (i.e. proc or - # lambda) to retrieve a specific record or customize the generated query - # when you access the associated object. - # - # Scope examples: - # has_one :author, -> { where(comment_id: 1) } - # has_one :employer, -> { joins(:company) } - # has_one :dob, ->(dob) { where("Date.new(2000, 01, 01) > ?", dob) } - # - # === Options - # - # The declaration can also include an +options+ hash to specialize the behavior of the association. - # - # Options are: - # [:class_name] - # Specify the class name of the association. Use it only if that name can't be inferred - # from the association name. So <tt>has_one :manager</tt> will by default be linked to the Manager class, but - # if the real class name is Person, you'll have to specify it with this option. - # [:dependent] - # Controls what happens to the associated object when - # its owner is destroyed: - # - # * <tt>:destroy</tt> causes the associated object to also be destroyed - # * <tt>:delete</tt> causes the associated object to be deleted directly from the database (so callbacks will not execute) - # * <tt>:nullify</tt> causes the foreign key to be set to +NULL+. Callbacks are not executed. - # * <tt>:restrict_with_exception</tt> causes an exception to be raised if there is an associated record - # * <tt>:restrict_with_error</tt> causes an error to be added to the owner if there is an associated object - # - # Note that <tt>:dependent</tt> option is ignored when using <tt>:through</tt> option. - # [:foreign_key] - # Specify the foreign key used for the association. By default this is guessed to be the name - # of this class in lower-case and "_id" suffixed. So a Person class that makes a +has_one+ association - # will use "person_id" as the default <tt>:foreign_key</tt>. - # [:foreign_type] - # Specify the column used to store the associated object's type, if this is a polymorphic - # association. By default this is guessed to be the name of the polymorphic association - # specified on "as" option with a "_type" suffix. So a class that defines a - # <tt>has_one :tag, as: :taggable</tt> association will use "taggable_type" as the - # default <tt>:foreign_type</tt>. - # [:primary_key] - # Specify the method that returns the primary key used for the association. By default this is +id+. - # [:as] - # Specifies a polymorphic interface (See <tt>belongs_to</tt>). - # [:through] - # Specifies a Join Model through which to perform the query. Options for <tt>:class_name</tt>, - # <tt>:primary_key</tt>, and <tt>:foreign_key</tt> are ignored, as the association uses the - # source reflection. You can only use a <tt>:through</tt> query through a <tt>has_one</tt> - # or <tt>belongs_to</tt> association on the join model. - # [:source] - # Specifies the source association name used by <tt>has_one :through</tt> queries. - # Only use it if the name cannot be inferred from the association. - # <tt>has_one :favorite, through: :favorites</tt> will look for a - # <tt>:favorite</tt> on Favorite, unless a <tt>:source</tt> is given. - # [:source_type] - # Specifies type of the source association used by <tt>has_one :through</tt> queries where the source - # association is a polymorphic +belongs_to+. - # [:validate] - # If +false+, don't validate the associated object when saving the parent object. +false+ by default. - # [:autosave] - # If true, always save the associated object or destroy it if marked for destruction, - # when saving the parent object. If false, never save or destroy the associated object. - # By default, only save the associated object if it's a new record. - # - # Note that <tt>accepts_nested_attributes_for</tt> sets <tt>:autosave</tt> to <tt>true</tt>. - # [:inverse_of] - # Specifies the name of the <tt>belongs_to</tt> association on the associated object - # that is the inverse of this <tt>has_one</tt> association. Does not work in combination - # with <tt>:through</tt> or <tt>:as</tt> options. - # See ActiveRecord::Associations::ClassMethods's overview on Bi-directional associations for more detail. - # [:required] - # When set to +true+, the association will also have its presence validated. - # This will validate the association itself, not the id. You can use - # +:inverse_of+ to avoid an extra query during validation. - # - # Option examples: - # 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 :attachment, as: :attachable - # has_one :boss, -> { readonly } - # has_one :club, through: :membership - # has_one :primary_address, -> { where primary: true }, through: :addressables, source: :addressable - # has_one :credit_card, required: true - def has_one(name, scope = nil, options = {}) - reflection = Builder::HasOne.build(self, name, scope, options) - Reflection.add_reflection self, name, reflection - end - - # Specifies a one-to-one association with another class. This method should only be used - # if this class contains the foreign key. If the other class contains the foreign key, - # then you should use +has_one+ instead. See also ActiveRecord::Associations::ClassMethods's overview - # on when to use +has_one+ and when to use +belongs_to+. - # - # Methods will be added for retrieval and query for a single associated object, for which - # this object holds an id: - # - # +association+ is a placeholder for the symbol passed as the +name+ argument, so - # <tt>belongs_to :author</tt> would add among others <tt>author.nil?</tt>. - # - # [association(force_reload = false)] - # Returns the associated object. +nil+ is returned if none is found. - # [association=(associate)] - # Assigns the associate object, extracts the primary key, and sets it as the foreign key. - # [build_association(attributes = {})] - # Returns a new object of the associated type that has been instantiated - # with +attributes+ and linked to this object through a foreign key, but has not yet been saved. - # [create_association(attributes = {})] - # Returns a new object of the associated type that has been instantiated - # with +attributes+, linked to this object through a foreign key, and that - # has already been saved (if it passed the validation). - # [create_association!(attributes = {})] - # Does the same as <tt>create_association</tt>, but raises <tt>ActiveRecord::RecordInvalid</tt> - # if the record is invalid. - # - # === Example - # - # A Post class declares <tt>belongs_to :author</tt>, which will add: - # * <tt>Post#author</tt> (similar to <tt>Author.find(author_id)</tt>) - # * <tt>Post#author=(author)</tt> (similar to <tt>post.author_id = author.id</tt>) - # * <tt>Post#build_author</tt> (similar to <tt>post.author = Author.new</tt>) - # * <tt>Post#create_author</tt> (similar to <tt>post.author = Author.new; post.author.save; post.author</tt>) - # * <tt>Post#create_author!</tt> (similar to <tt>post.author = Author.new; post.author.save!; post.author</tt>) - # The declaration can also include an +options+ hash to specialize the behavior of the association. - # - # === Scopes - # - # You can pass a second argument +scope+ as a callable (i.e. proc or - # lambda) to retrieve a specific record or customize the generated query - # when you access the associated object. - # - # Scope examples: - # belongs_to :firm, -> { where(id: 2) } - # belongs_to :user, -> { joins(:friends) } - # belongs_to :level, ->(level) { where("game_level > ?", level.current) } - # - # === Options - # - # [:class_name] - # Specify the class name of the association. Use it only if that name can't be inferred - # from the association name. So <tt>belongs_to :author</tt> will by default be linked to the Author class, but - # if the real class name is Person, you'll have to specify it with this option. - # [:foreign_key] - # Specify the foreign key used for the association. By default this is guessed to be the name - # of the association with an "_id" suffix. So a class that defines a <tt>belongs_to :person</tt> - # association will use "person_id" as the default <tt>:foreign_key</tt>. Similarly, - # <tt>belongs_to :favorite_person, class_name: "Person"</tt> will use a foreign key - # of "favorite_person_id". - # [:foreign_type] - # Specify the column used to store the associated object's type, if this is a polymorphic - # association. By default this is guessed to be the name of the association with a "_type" - # suffix. So a class that defines a <tt>belongs_to :taggable, polymorphic: true</tt> - # association will use "taggable_type" as the default <tt>:foreign_type</tt>. - # [:primary_key] - # Specify the method that returns the primary key of associated object used for the association. - # By default this is id. - # [:dependent] - # If set to <tt>:destroy</tt>, the associated object is destroyed when this object is. If set to - # <tt>:delete</tt>, the associated object is deleted *without* calling its destroy method. - # This option should not be specified when <tt>belongs_to</tt> is used in conjunction with - # a <tt>has_many</tt> relationship on another class because of the potential to leave - # orphaned records behind. - # [:counter_cache] - # Caches the number of belonging objects on the associate class through the use of +increment_counter+ - # and +decrement_counter+. The counter cache is incremented when an object of this - # class is created and decremented when it's destroyed. This requires that a column - # named <tt>#{table_name}_count</tt> (such as +comments_count+ for a belonging Comment class) - # is used on the associate class (such as a Post class) - that is the migration for - # <tt>#{table_name}_count</tt> is created on the associate class (such that <tt>Post.comments_count</tt> will - # return the count cached, see note below). You can also specify a custom counter - # cache column by providing a column name instead of a +true+/+false+ value to this - # option (e.g., <tt>counter_cache: :my_custom_counter</tt>.) - # Note: Specifying a counter cache will add it to that model's list of readonly attributes - # using +attr_readonly+. - # [:polymorphic] - # Specify this association is a polymorphic association by passing +true+. - # Note: If you've enabled the counter cache, then you may want to add the counter cache attribute - # to the +attr_readonly+ list in the associated classes (e.g. <tt>class Post; attr_readonly :comments_count; end</tt>). - # [:validate] - # If +false+, don't validate the associated objects when saving the parent object. +false+ by default. - # [:autosave] - # If true, always save the associated object or destroy it if marked for destruction, when - # saving the parent object. - # If false, never save or destroy the associated object. - # By default, only save the associated object if it's a new record. - # - # Note that <tt>accepts_nested_attributes_for</tt> sets <tt>:autosave</tt> to <tt>true</tt>. - # [:touch] - # If true, the associated object will be touched (the updated_at/on attributes set to current time) - # when this record is either saved or destroyed. If you specify a symbol, that attribute - # will be updated with the current time in addition to the updated_at/on attribute. - # [:inverse_of] - # Specifies the name of the <tt>has_one</tt> or <tt>has_many</tt> association on the associated - # object that is the inverse of this <tt>belongs_to</tt> association. Does not work in - # combination with the <tt>:polymorphic</tt> options. - # See ActiveRecord::Associations::ClassMethods's overview on Bi-directional associations for more detail. - # [:optional] - # When set to +true+, the association will not have its presence validated. - # [:required] - # When set to +true+, the association will also have its presence validated. - # This will validate the association itself, not the id. You can use - # +:inverse_of+ to avoid an extra query during validation. - # NOTE: <tt>required</tt> is set to <tt>true</tt> by default and is deprecated. If - # you don't want to have association presence validated, use <tt>optional: true</tt>. - # - # Option examples: - # belongs_to :firm, foreign_key: "client_of" - # belongs_to :person, primary_key: "name", foreign_key: "person_name" - # belongs_to :author, class_name: "Person", foreign_key: "author_id" - # belongs_to :valid_coupon, ->(o) { where "discounts > ?", o.payments_count }, - # class_name: "Coupon", foreign_key: "coupon_id" - # belongs_to :attachable, polymorphic: true - # belongs_to :project, -> { readonly } - # belongs_to :post, counter_cache: true - # belongs_to :comment, touch: true - # belongs_to :company, touch: :employees_last_updated_at - # belongs_to :user, optional: true - def belongs_to(name, scope = nil, options = {}) - reflection = Builder::BelongsTo.build(self, name, scope, options) - Reflection.add_reflection self, name, reflection - end - - # Specifies a many-to-many relationship with another class. This associates two classes via an - # intermediate join table. Unless the join table is explicitly specified as an option, it is - # guessed using the lexical order of the class names. So a join between Developer and Project - # will give the default join table name of "developers_projects" because "D" precedes "P" alphabetically. - # Note that this precedence is calculated using the <tt><</tt> operator for String. This - # means that if the strings are of different lengths, and the strings are equal when compared - # up to the shortest length, then the longer string is considered of higher - # lexical precedence than the shorter one. For example, one would expect the tables "paper_boxes" and "papers" - # to generate a join table name of "papers_paper_boxes" because of the length of the name "paper_boxes", - # but it in fact generates a join table name of "paper_boxes_papers". Be aware of this caveat, and use the - # custom <tt>:join_table</tt> option if you need to. - # If your tables share a common prefix, it will only appear once at the beginning. For example, - # the tables "catalog_categories" and "catalog_products" generate a join table name of "catalog_categories_products". - # - # The join table should not have a primary key or a model associated with it. You must manually generate the - # join table with a migration such as this: - # - # class CreateDevelopersProjectsJoinTable < ActiveRecord::Migration - # def change - # create_join_table :developers, :projects + # If your model class is <tt>Project</tt>, then the module is + # named <tt>Project::GeneratedAssociationMethods</tt>. The +GeneratedAssociationMethods+ module is + # included in the model class immediately after the (anonymous) generated attributes methods + # module, meaning an association will override the methods for an attribute with the same name. + # + # == Cardinality and associations + # + # Active Record associations can be used to describe one-to-one, one-to-many and many-to-many + # relationships between models. Each model uses an association to describe its role in + # the relation. The #belongs_to association is always used in the model that has + # the foreign key. + # + # === One-to-one + # + # Use #has_one in the base, and #belongs_to in the associated model. + # + # class Employee < ActiveRecord::Base + # has_one :office + # end + # class Office < ActiveRecord::Base + # belongs_to :employee # foreign key - employee_id + # end + # + # === One-to-many + # + # Use #has_many in the base, and #belongs_to in the associated model. + # + # class Manager < ActiveRecord::Base + # has_many :employees + # end + # class Employee < ActiveRecord::Base + # belongs_to :manager # foreign key - manager_id + # end + # + # === Many-to-many + # + # There are two ways to build a many-to-many relationship. + # + # The first way uses a #has_many association with the <tt>:through</tt> option and a join model, so + # there are two stages of associations. + # + # class Assignment < ActiveRecord::Base + # belongs_to :programmer # foreign key - programmer_id + # belongs_to :project # foreign key - project_id + # end + # class Programmer < ActiveRecord::Base + # has_many :assignments + # has_many :projects, through: :assignments + # end + # class Project < ActiveRecord::Base + # has_many :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 + # that has no corresponding model or primary key. + # + # class Programmer < ActiveRecord::Base + # has_and_belongs_to_many :projects # foreign keys in the join table + # end + # class Project < ActiveRecord::Base + # has_and_belongs_to_many :programmers # foreign keys in the join table + # end + # + # Choosing which way to build a many-to-many relationship is not always simple. + # If you need to work with the relationship model as its own entity, + # use #has_many <tt>:through</tt>. Use #has_and_belongs_to_many when working with legacy schemas or when + # you never work directly with the relationship itself. + # + # == Is it a #belongs_to or #has_one association? + # + # Both express a 1-1 relationship. The difference is mostly where to place the foreign + # key, which goes on the table for the class declaring the #belongs_to relationship. + # + # class User < ActiveRecord::Base + # # I reference an account. + # belongs_to :account + # end + # + # class Account < ActiveRecord::Base + # # One user references me. + # has_one :user + # end + # + # The tables for these classes could look something like: + # + # CREATE TABLE users ( + # id int NOT NULL auto_increment, + # account_id int default NULL, + # name varchar default NULL, + # PRIMARY KEY (id) + # ) + # + # CREATE TABLE accounts ( + # id int NOT NULL auto_increment, + # name varchar default NULL, + # PRIMARY KEY (id) + # ) + # + # == Unsaved objects and associations + # + # You can manipulate objects and associations before they are saved to the database, but + # there is some special behavior you should be aware of, mostly involving the saving of + # associated objects. + # + # You can set the <tt>:autosave</tt> option on a #has_one, #belongs_to, + # #has_many, or #has_and_belongs_to_many association. Setting it + # to +true+ will _always_ save the members, whereas setting it to +false+ will + # _never_ save the members. More details about <tt>:autosave</tt> option is available at + # AutosaveAssociation. + # + # === One-to-one associations + # + # * Assigning an object to a #has_one association automatically saves that object and + # the object being replaced (if there is one), in order to update their foreign + # keys - except if the parent object is unsaved (<tt>new_record? == true</tt>). + # * If either of these saves fail (due to one of the objects being invalid), an + # ActiveRecord::RecordNotSaved exception is raised and the assignment is + # cancelled. + # * If you wish to assign an object to a #has_one association without saving it, + # use the <tt>#build_association</tt> method (documented below). The object being + # replaced will still be saved to update its foreign key. + # * Assigning an object to a #belongs_to association does not save the object, since + # the foreign key field belongs on the parent. It does not save the parent either. + # + # === Collections + # + # * Adding an object to a collection (#has_many or #has_and_belongs_to_many) automatically + # saves that object, except if the parent object (the owner of the collection) is not yet + # stored in the database. + # * If saving any of the objects being added to a collection (via <tt>push</tt> or similar) + # fails, then <tt>push</tt> returns +false+. + # * If saving fails while replacing the collection (via <tt>association=</tt>), an + # ActiveRecord::RecordNotSaved exception is raised and the assignment is + # cancelled. + # * You can add an object to a collection without automatically saving it by using the + # <tt>collection.build</tt> method (documented below). + # * All unsaved (<tt>new_record? == true</tt>) members of the collection are automatically + # saved when the parent is saved. + # + # == Customizing the query + # + # \Associations are built from <tt>Relation</tt> objects, and you can use the Relation syntax + # to customize them. For example, to add a condition: + # + # class Blog < ActiveRecord::Base + # has_many :published_posts, -> { where(published: true) }, class_name: 'Post' + # end + # + # Inside the <tt>-> { ... }</tt> block you can use all of the usual Relation methods. + # + # === Accessing the owner object + # + # Sometimes it is useful to have access to the owner object when building the query. The owner + # is passed as a parameter to the block. For example, the following association would find all + # events that occur on the user's birthday: + # + # class User < ActiveRecord::Base + # has_many :birthday_events, ->(user) { where(starts_on: user.birthday) }, class_name: 'Event' + # end + # + # Note: Joining, eager loading and preloading of these associations is not fully possible. + # These operations happen before instance creation and the scope will be called with a +nil+ argument. + # This can lead to unexpected behavior and is deprecated. + # + # == Association callbacks + # + # Similar to the normal callbacks that hook into the life cycle of an Active Record object, + # you can also define callbacks that get triggered when you add an object to or remove an + # object from an association collection. + # + # class Project + # has_and_belongs_to_many :developers, after_add: :evaluate_velocity + # + # def evaluate_velocity(developer) + # ... + # end + # end + # + # It's possible to stack callbacks by passing them as an array. Example: + # + # class Project + # has_and_belongs_to_many :developers, + # after_add: [:evaluate_velocity, Proc.new { |p, d| p.shipping_date = Time.now}] + # end + # + # Possible callbacks are: +before_add+, +after_add+, +before_remove+ and +after_remove+. + # + # If any of the +before_add+ callbacks throw an exception, the object will not be + # added to the collection. + # + # Similarly, if any of the +before_remove+ callbacks throw an exception, the object + # will not be removed from the collection. + # + # == Association extensions + # + # The proxy objects that control the access to associations can be extended through anonymous + # modules. This is especially beneficial for adding new finders, creators, and other + # factory-type methods that are only used as part of this association. + # + # class Account < ActiveRecord::Base + # has_many :people do + # def find_or_create_by_name(name) + # first_name, last_name = name.split(" ", 2) + # find_or_create_by(first_name: first_name, last_name: last_name) + # end # end # end # - # It's also a good idea to add indexes to each of those columns to speed up the joins process. - # However, in MySQL it is advised to add a compound index for both of the columns as MySQL only - # uses one index per table during the lookup. - # - # Adds the following methods for retrieval and query: - # - # +collection+ is a placeholder for the symbol passed as the +name+ argument, so - # <tt>has_and_belongs_to_many :categories</tt> would add among others <tt>categories.empty?</tt>. - # - # [collection(force_reload = false)] - # Returns an array of all the associated objects. - # An empty array is returned if none are found. - # [collection<<(object, ...)] - # Adds one or more objects to the collection by creating associations in the join table - # (<tt>collection.push</tt> and <tt>collection.concat</tt> are aliases to this method). - # Note that this operation instantly fires update SQL without waiting for the save or update call on the - # parent object, unless the parent object is a new record. - # [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] - # Returns an array of the associated objects' ids. - # [collection_singular_ids=ids] - # Replace the collection by the objects identified by the primary keys in +ids+. - # [collection.clear] - # Removes every object from the collection. This does not destroy the objects. - # [collection.empty?] - # Returns +true+ if there are no associated objects. - # [collection.size] - # Returns the number of associated objects. - # [collection.find(id)] - # Finds an associated object responding to the +id+ and that - # meets the condition that it has to be associated with this object. - # Uses the same rules as <tt>ActiveRecord::Base.find</tt>. - # [collection.exists?(...)] - # Checks whether an associated object with the given conditions exists. - # Uses the same rules as <tt>ActiveRecord::Base.exists?</tt>. - # [collection.build(attributes = {})] - # Returns a new object of the collection type that has been instantiated - # with +attributes+ and linked to this object through the join table, but has not yet been saved. - # [collection.create(attributes = {})] - # Returns a new object of the collection type that has been instantiated - # with +attributes+, linked to this object through the join table, and that has already been - # saved (if it passed the validation). - # - # === Example - # - # A Developer class declares <tt>has_and_belongs_to_many :projects</tt>, which will add: - # * <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> - # * <tt>Developer#projects.clear</tt> - # * <tt>Developer#projects.empty?</tt> - # * <tt>Developer#projects.size</tt> - # * <tt>Developer#projects.find(id)</tt> - # * <tt>Developer#projects.exists?(...)</tt> - # * <tt>Developer#projects.build</tt> (similar to <tt>Project.new("developer_id" => id)</tt>) - # * <tt>Developer#projects.create</tt> (similar to <tt>c = Project.new("developer_id" => id); c.save; c</tt>) - # The declaration may include an +options+ hash to specialize the behavior of the association. - # - # === Scopes - # - # You can pass a second argument +scope+ as a callable (i.e. proc or - # lambda) to retrieve a specific set of records or customize the generated - # query when you access the associated collection. - # - # Scope examples: - # has_and_belongs_to_many :projects, -> { includes :milestones, :manager } - # has_and_belongs_to_many :categories, ->(category) { - # where("default_category = ?", category.name) - # } - # - # === Extensions - # - # The +extension+ argument allows you to pass a block into a - # has_and_belongs_to_many association. This is useful for adding new - # finders, creators and other factory-type methods to be used as part of - # the association. - # - # Extension examples: - # has_and_belongs_to_many :contractors do + # person = Account.first.people.find_or_create_by_name("David Heinemeier Hansson") + # person.first_name # => "David" + # person.last_name # => "Heinemeier Hansson" + # + # If you need to share the same extensions between many associations, you can use a named + # extension module. + # + # module FindOrCreateByNameExtension # def find_or_create_by_name(name) # first_name, last_name = name.split(" ", 2) # find_or_create_by(first_name: first_name, last_name: last_name) # end # end # - # === Options - # - # [:class_name] - # Specify the class name of the association. Use it only if that name can't be inferred - # from the association name. So <tt>has_and_belongs_to_many :projects</tt> will by default be linked to the - # Project class, but if the real class name is SuperProject, you'll have to specify it with this option. - # [:join_table] - # Specify the name of the join table if the default based on lexical order isn't what you want. - # <b>WARNING:</b> If you're overwriting the table name of either class, the +table_name+ method - # MUST be declared underneath any +has_and_belongs_to_many+ declaration in order to work. - # [:foreign_key] - # Specify the foreign key used for the association. By default this is guessed to be the name - # of this class in lower-case and "_id" suffixed. So a Person class that makes - # a +has_and_belongs_to_many+ association to Project will use "person_id" as the - # default <tt>:foreign_key</tt>. - # [:association_foreign_key] - # Specify the foreign key used for the association on the receiving side of the association. - # By default this is guessed to be the name of the associated class in lower-case and "_id" suffixed. - # So if a Person class makes a +has_and_belongs_to_many+ association to Project, - # the association will use "project_id" as the default <tt>:association_foreign_key</tt>. - # [:readonly] - # If true, all the associated objects are readonly through the association. - # [:validate] - # If +false+, don't validate the associated objects when saving the parent object. +true+ by default. - # [:autosave] - # If true, always save the associated objects or destroy them if marked for destruction, when - # saving the parent object. - # If false, never save or destroy the associated objects. - # By default, only save associated objects that are new records. - # - # Note that <tt>accepts_nested_attributes_for</tt> sets <tt>:autosave</tt> to <tt>true</tt>. - # - # Option examples: - # has_and_belongs_to_many :projects - # has_and_belongs_to_many :projects, -> { includes :milestones, :manager } - # has_and_belongs_to_many :nations, class_name: "Country" - # has_and_belongs_to_many :categories, join_table: "prods_cats" - # has_and_belongs_to_many :categories, -> { readonly } - def has_and_belongs_to_many(name, scope = nil, options = {}, &extension) - if scope.is_a?(Hash) - options = scope - scope = nil + # class Account < ActiveRecord::Base + # has_many :people, -> { extending FindOrCreateByNameExtension } + # end + # + # class Company < ActiveRecord::Base + # has_many :people, -> { extending FindOrCreateByNameExtension } + # end + # + # Some extensions can only be made to work with knowledge of the association's internals. + # Extensions can access relevant state using the following methods (where +items+ is the + # name of the association): + # + # * <tt>record.association(:items).owner</tt> - Returns the object the association is part of. + # * <tt>record.association(:items).reflection</tt> - Returns the reflection object that describes the association. + # * <tt>record.association(:items).target</tt> - Returns the associated object for #belongs_to and #has_one, or + # the collection of associated objects for #has_many and #has_and_belongs_to_many. + # + # However, inside the actual extension code, you will not have access to the <tt>record</tt> as + # above. In this case, you can access <tt>proxy_association</tt>. For example, + # <tt>record.association(:items)</tt> and <tt>record.items.proxy_association</tt> will return + # the same object, allowing you to make calls like <tt>proxy_association.owner</tt> inside + # association extensions. + # + # == Association Join Models + # + # Has Many associations can be configured with the <tt>:through</tt> option to use an + # explicit join model to retrieve the data. This operates similarly to a + # #has_and_belongs_to_many association. The advantage is that you're able to add validations, + # callbacks, and extra attributes on the join model. Consider the following schema: + # + # class Author < ActiveRecord::Base + # has_many :authorships + # has_many :books, through: :authorships + # end + # + # class Authorship < ActiveRecord::Base + # belongs_to :author + # belongs_to :book + # end + # + # @author = Author.first + # @author.authorships.collect { |a| a.book } # selects all books that the author's authorships belong to + # @author.books # selects all books by using the Authorship join model + # + # You can also go through a #has_many association on the join model: + # + # class Firm < ActiveRecord::Base + # has_many :clients + # has_many :invoices, through: :clients + # end + # + # class Client < ActiveRecord::Base + # belongs_to :firm + # has_many :invoices + # end + # + # class Invoice < ActiveRecord::Base + # belongs_to :client + # end + # + # @firm = Firm.first + # @firm.clients.flat_map { |c| c.invoices } # select all invoices for all clients of the firm + # @firm.invoices # selects all invoices by going through the Client join model + # + # Similarly you can go through a #has_one association on the join model: + # + # class Group < ActiveRecord::Base + # has_many :users + # has_many :avatars, through: :users + # end + # + # class User < ActiveRecord::Base + # belongs_to :group + # has_one :avatar + # end + # + # class Avatar < ActiveRecord::Base + # belongs_to :user + # end + # + # @group = Group.first + # @group.users.collect { |u| u.avatar }.compact # select all avatars for all users in the group + # @group.avatars # selects all avatars by going through the User join model. + # + # An important caveat with going through #has_one or #has_many associations on the + # join model is that these associations are *read-only*. For example, the following + # would not work following the previous example: + # + # @group.avatars << Avatar.new # this would work if User belonged_to Avatar rather than the other way around + # @group.avatars.delete(@group.avatars.last) # so would this + # + # == Setting Inverses + # + # If you are using a #belongs_to on the join model, it is a good idea to set the + # <tt>:inverse_of</tt> option on the #belongs_to, which will mean that the following example + # works correctly (where <tt>tags</tt> is a #has_many <tt>:through</tt> association): + # + # @post = Post.first + # @tag = @post.tags.build name: "ruby" + # @tag.save + # + # The last line ought to save the through record (a <tt>Tagging</tt>). This will only work if the + # <tt>:inverse_of</tt> is set: + # + # class Tagging < ActiveRecord::Base + # belongs_to :post + # belongs_to :tag, inverse_of: :taggings + # end + # + # If you do not set the <tt>:inverse_of</tt> record, the association will + # do its best to match itself up with the correct inverse. Automatic + # inverse detection only works on #has_many, #has_one, and + # #belongs_to associations. + # + # Extra options on the associations, as defined in the + # <tt>AssociationReflection::INVALID_AUTOMATIC_INVERSE_OPTIONS</tt> constant, will + # also prevent the association's inverse from being found automatically. + # + # The automatic guessing of the inverse association uses a heuristic based + # on the name of the class, so it may not work for all associations, + # especially the ones with non-standard names. + # + # You can turn off the automatic detection of inverse associations by setting + # the <tt>:inverse_of</tt> option to <tt>false</tt> like so: + # + # class Tagging < ActiveRecord::Base + # belongs_to :tag, inverse_of: false + # end + # + # == Nested \Associations + # + # You can actually specify *any* association with the <tt>:through</tt> option, including an + # association which has a <tt>:through</tt> option itself. For example: + # + # class Author < ActiveRecord::Base + # has_many :posts + # has_many :comments, through: :posts + # has_many :commenters, through: :comments + # end + # + # class Post < ActiveRecord::Base + # has_many :comments + # end + # + # class Comment < ActiveRecord::Base + # belongs_to :commenter + # end + # + # @author = Author.first + # @author.commenters # => People who commented on posts written by the author + # + # An equivalent way of setting up this association this would be: + # + # class Author < ActiveRecord::Base + # has_many :posts + # has_many :commenters, through: :posts + # end + # + # class Post < ActiveRecord::Base + # has_many :comments + # has_many :commenters, through: :comments + # end + # + # class Comment < ActiveRecord::Base + # belongs_to :commenter + # end + # + # When using a nested association, you will not be able to modify the association because there + # is not enough information to know what modification to make. For example, if you tried to + # add a <tt>Commenter</tt> in the example above, there would be no way to tell how to set up the + # intermediate <tt>Post</tt> and <tt>Comment</tt> objects. + # + # == Polymorphic \Associations + # + # Polymorphic associations on models are not restricted on what types of models they + # can be associated with. Rather, they specify an interface that a #has_many association + # must adhere to. + # + # class Asset < ActiveRecord::Base + # belongs_to :attachable, polymorphic: true + # end + # + # class Post < ActiveRecord::Base + # has_many :assets, as: :attachable # The :as option specifies the polymorphic interface to use. + # end + # + # @asset.attachable = @post + # + # This works by using a type column in addition to a foreign key to specify the associated + # record. In the Asset example, you'd need an +attachable_id+ integer column and an + # +attachable_type+ string column. + # + # Using polymorphic associations in combination with single table inheritance (STI) is + # a little tricky. In order for the associations to work as expected, ensure that you + # store the base model for the STI models in the type column of the polymorphic + # association. To continue with the asset example above, suppose there are guest posts + # and member posts that use the posts table for STI. In this case, there must be a +type+ + # column in the posts table. + # + # Note: The <tt>attachable_type=</tt> method is being called when assigning an +attachable+. + # The +class_name+ of the +attachable+ is passed as a String. + # + # class Asset < ActiveRecord::Base + # belongs_to :attachable, polymorphic: true + # + # def attachable_type=(class_name) + # super(class_name.constantize.base_class.to_s) + # end + # end + # + # class Post < ActiveRecord::Base + # # because we store "Post" in attachable_type now dependent: :destroy will work + # has_many :assets, as: :attachable, dependent: :destroy + # end + # + # class GuestPost < Post + # end + # + # class MemberPost < Post + # end + # + # == Caching + # + # All of the methods are built on a simple caching principle that will keep the result + # of the last query around unless specifically instructed not to. The cache is even + # shared across methods to make it even cheaper to use the macro-added methods without + # worrying too much about performance at the first go. + # + # project.milestones # fetches milestones from the database + # project.milestones.size # uses the milestone cache + # project.milestones.empty? # uses the milestone cache + # project.milestones(true).size # fetches milestones from the database + # project.milestones # uses the milestone cache + # + # == Eager loading of associations + # + # Eager loading is a way to find objects of a certain class and a number of named associations. + # It is one of the easiest ways to prevent the dreaded N+1 problem in which fetching 100 + # posts that each need to display their author triggers 101 database queries. Through the + # use of eager loading, the number of queries will be reduced from 101 to 2. + # + # class Post < ActiveRecord::Base + # belongs_to :author + # has_many :comments + # end + # + # Consider the following loop using the class above: + # + # Post.all.each do |post| + # puts "Post: " + post.title + # puts "Written by: " + post.author.name + # puts "Last comment on: " + post.comments.first.created_on + # end + # + # To iterate over these one hundred posts, we'll generate 201 database queries. Let's + # first just optimize it for retrieving the author: + # + # Post.includes(:author).each do |post| + # + # This references the name of the #belongs_to association that also used the <tt>:author</tt> + # symbol. After loading the posts, +find+ will collect the +author_id+ from each one and load + # all of the referenced authors with one query. Doing so will cut down the number of queries + # from 201 to 102. + # + # We can improve upon the situation further by referencing both associations in the finder with: + # + # Post.includes(:author, :comments).each do |post| + # + # This will load all comments with a single query. This reduces the total number of queries + # to 3. In general, the number of queries will be 1 plus the number of associations + # named (except if some of the associations are polymorphic #belongs_to - see below). + # + # To include a deep hierarchy of associations, use a hash: + # + # Post.includes(:author, { comments: { author: :gravatar } }).each do |post| + # + # The above code will load all the comments and all of their associated + # authors and gravatars. You can mix and match any combination of symbols, + # arrays, and hashes to retrieve the associations you want to load. + # + # All of this power shouldn't fool you into thinking that you can pull out huge amounts + # of data with no performance penalty just because you've reduced the number of queries. + # The database still needs to send all the data to Active Record and it still needs to + # be processed. So it's no catch-all for performance problems, but it's a great way to + # cut down on the number of queries in a situation as the one described above. + # + # Since only one table is loaded at a time, conditions or orders cannot reference tables + # other than the main one. If this is the case, Active Record falls back to the previously + # used <tt>LEFT OUTER JOIN</tt> based strategy. For example: + # + # Post.includes([:author, :comments]).where(['comments.approved = ?', true]) + # + # This will result in a single SQL query with joins along the lines of: + # <tt>LEFT OUTER JOIN comments ON comments.post_id = posts.id</tt> and + # <tt>LEFT OUTER JOIN authors ON authors.id = posts.author_id</tt>. Note that using conditions + # like this can have unintended consequences. + # In the above example, posts with no approved comments are not returned at all because + # the conditions apply to the SQL statement as a whole and not just to the association. + # + # You must disambiguate column references for this fallback to happen, for example + # <tt>order: "author.name DESC"</tt> will work but <tt>order: "name DESC"</tt> will not. + # + # If you want to load all posts (including posts with no approved comments), then write + # your own <tt>LEFT OUTER JOIN</tt> query using <tt>ON</tt>: + # + # Post.joins("LEFT OUTER JOIN comments ON comments.post_id = posts.id AND comments.approved = '1'") + # + # In this case, it is usually more natural to include an association which has conditions defined on it: + # + # class Post < ActiveRecord::Base + # has_many :approved_comments, -> { where(approved: true) }, class_name: 'Comment' + # end + # + # Post.includes(:approved_comments) + # + # This will load posts and eager load the +approved_comments+ association, which contains + # only those comments that have been approved. + # + # If you eager load an association with a specified <tt>:limit</tt> option, it will be ignored, + # returning all the associated objects: + # + # class Picture < ActiveRecord::Base + # 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. + # + # Eager loading is supported with polymorphic associations. + # + # class Address < ActiveRecord::Base + # belongs_to :addressable, polymorphic: true + # end + # + # A call that tries to eager load the addressable model + # + # Address.includes(:addressable) + # + # This will execute one query to load the addresses and load the addressables with one + # query per addressable type. + # For example, if all the addressables are either of class Person or Company, then a total + # of 3 queries will be executed. The list of addressable types to load is determined on + # the back of the addresses loaded. This is not supported if Active Record has to fallback + # to the previous implementation of eager loading and will raise ActiveRecord::EagerLoadPolymorphicError. + # The reason is that the parent model's type is a column value so its corresponding table + # name cannot be put in the +FROM+/+JOIN+ clauses of that query. + # + # == Table Aliasing + # + # Active Record uses table aliasing in the case that a table is referenced multiple times + # in a join. If a table is referenced only once, the standard table name is used. The + # second time, the table is aliased as <tt>#{reflection_name}_#{parent_table_name}</tt>. + # Indexes are appended for any more successive uses of the table name. + # + # Post.joins(:comments) + # # => SELECT ... FROM posts INNER JOIN comments ON ... + # Post.joins(:special_comments) # STI + # # => SELECT ... FROM posts INNER JOIN comments ON ... AND comments.type = 'SpecialComment' + # Post.joins(:comments, :special_comments) # special_comments is the reflection name, posts is the parent table name + # # => SELECT ... FROM posts INNER JOIN comments ON ... INNER JOIN comments special_comments_posts + # + # Acts as tree example: + # + # TreeMixin.joins(:children) + # # => SELECT ... FROM mixins INNER JOIN mixins childrens_mixins ... + # TreeMixin.joins(children: :parent) + # # => SELECT ... FROM mixins INNER JOIN mixins childrens_mixins ... + # INNER JOIN parents_mixins ... + # TreeMixin.joins(children: {parent: :children}) + # # => SELECT ... FROM mixins INNER JOIN mixins childrens_mixins ... + # INNER JOIN parents_mixins ... + # INNER JOIN mixins childrens_mixins_2 + # + # Has and Belongs to Many join tables use the same idea, but add a <tt>_join</tt> suffix: + # + # Post.joins(:categories) + # # => SELECT ... FROM posts INNER JOIN categories_posts ... INNER JOIN categories ... + # 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}) + # # => 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 + # + # If you wish to specify your own custom joins using ActiveRecord::QueryMethods#joins method, those table + # names will take precedence over the eager associations: + # + # Post.joins(:comments).joins("inner join comments ...") + # # => SELECT ... FROM posts INNER JOIN comments_posts ON ... INNER JOIN comments ... + # Post.joins(:comments, :special_comments).joins("inner join comments ...") + # # => SELECT ... FROM posts INNER JOIN comments comments_posts ON ... + # INNER JOIN comments special_comments_posts ... + # INNER JOIN comments ... + # + # Table aliases are automatically truncated according to the maximum length of table identifiers + # according to the specific database. + # + # == Modules + # + # By default, associations will look for objects within the current module scope. Consider: + # + # module MyApplication + # module Business + # class Firm < ActiveRecord::Base + # has_many :clients + # end + # + # class Client < ActiveRecord::Base; end + # end + # end + # + # When <tt>Firm#clients</tt> is called, it will in turn call + # <tt>MyApplication::Business::Client.find_all_by_firm_id(firm.id)</tt>. + # If you want to associate with a class in another module scope, this can be done by + # specifying the complete class name. + # + # module MyApplication + # module Business + # class Firm < ActiveRecord::Base; end + # end + # + # module Billing + # class Account < ActiveRecord::Base + # belongs_to :firm, class_name: "MyApplication::Business::Firm" + # end + # end + # end + # + # == Bi-directional associations + # + # When you specify an association, there is usually an association on the associated model + # that specifies the same relationship in reverse. For example, with the following models: + # + # class Dungeon < ActiveRecord::Base + # has_many :traps + # has_one :evil_wizard + # end + # + # class Trap < ActiveRecord::Base + # belongs_to :dungeon + # end + # + # class EvilWizard < ActiveRecord::Base + # belongs_to :dungeon + # end + # + # The +traps+ association on +Dungeon+ and the +dungeon+ association on +Trap+ are + # the inverse of each other, and the inverse of the +dungeon+ association on +EvilWizard+ + # is the +evil_wizard+ association on +Dungeon+ (and vice-versa). By default, + # Active Record can guess the inverse of the association based on the name + # of the class. The result is the following: + # + # d = Dungeon.first + # t = d.traps.first + # d.object_id == t.dungeon.object_id # => true + # + # The +Dungeon+ instances +d+ and <tt>t.dungeon</tt> in the above example refer to + # the same in-memory instance since the association matches the name of the class. + # The result would be the same if we added +:inverse_of+ to our model definitions: + # + # class Dungeon < ActiveRecord::Base + # has_many :traps, inverse_of: :dungeon + # has_one :evil_wizard, inverse_of: :dungeon + # end + # + # class Trap < ActiveRecord::Base + # belongs_to :dungeon, inverse_of: :traps + # end + # + # class EvilWizard < ActiveRecord::Base + # belongs_to :dungeon, inverse_of: :evil_wizard + # end + # + # There are limitations to <tt>:inverse_of</tt> support: + # + # * does not work with <tt>:through</tt> associations. + # * does not work with <tt>:polymorphic</tt> associations. + # * inverse associations for #belongs_to associations #has_many are ignored. + # + # For more information, see the documentation for the +:inverse_of+ option. + # + # == Deleting from associations + # + # === Dependent associations + # + # #has_many, #has_one, and #belongs_to associations support the <tt>:dependent</tt> option. + # This allows you to specify that associated records should be deleted when the owner is + # deleted. + # + # For example: + # + # class Author + # has_many :posts, dependent: :destroy + # end + # Author.find(1).destroy # => Will destroy all of the author's posts, too + # + # The <tt>:dependent</tt> option can have different values which specify how the deletion + # is done. For more information, see the documentation for this option on the different + # specific association types. When no option is given, the behavior 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. + # + # Note that <tt>:dependent</tt> option is ignored for #has_one <tt>:through</tt> associations. + # + # === Delete or destroy? + # + # #has_many and #has_and_belongs_to_many associations have the methods <tt>destroy</tt>, + # <tt>delete</tt>, <tt>destroy_all</tt> and <tt>delete_all</tt>. + # + # For #has_and_belongs_to_many, <tt>delete</tt> and <tt>destroy</tt> are the same: they + # cause the records in the join table to be removed. + # + # For #has_many, <tt>destroy</tt> and <tt>destroy_all</tt> will always call the <tt>destroy</tt> method of the + # record(s) being removed so that callbacks are run. However <tt>delete</tt> and <tt>delete_all</tt> will either + # do the deletion according to the strategy specified by the <tt>:dependent</tt> option, or + # if no <tt>:dependent</tt> option is given, then it will follow the default strategy. + # The default strategy is to do nothing (leave the foreign keys with the parent ids set), except for + # #has_many <tt>:through</tt>, where the default strategy is <tt>delete_all</tt> (delete + # the join records, without running their callbacks). + # + # There is also a <tt>clear</tt> method which is the same as <tt>delete_all</tt>, except that + # it returns the association rather than the records which have been deleted. + # + # === What gets deleted? + # + # There is a potential pitfall here: #has_and_belongs_to_many and #has_many <tt>:through</tt> + # associations have records in join tables, as well as the associated records. So when we + # call one of these deletion methods, what exactly should be deleted? + # + # The answer is that it is assumed that deletion on an association is about removing the + # <i>link</i> between the owner and the associated object(s), rather than necessarily the + # associated objects themselves. So with #has_and_belongs_to_many and #has_many + # <tt>:through</tt>, the join records will be deleted, but the associated records won't. + # + # This makes sense if you think about it: if you were to call <tt>post.tags.delete(Tag.find_by(name: 'food'))</tt> + # you would want the 'food' tag to be unlinked from the post, rather than for the tag itself + # to be removed from the database. + # + # However, there are examples where this strategy doesn't make sense. For example, suppose + # a person has many projects, and each project has many tasks. If we deleted one of a person's + # tasks, we would probably not want the project to be deleted. In this scenario, the delete method + # won't actually work: it can only be used if the association on the join model is a + # #belongs_to. In other situations you are expected to perform operations directly on + # either the associated records or the <tt>:through</tt> association. + # + # With a regular #has_many there is no distinction between the "associated records" + # and the "link", so there is only one choice for what gets deleted. + # + # With #has_and_belongs_to_many and #has_many <tt>:through</tt>, if you want to delete the + # associated records themselves, you can always do something along the lines of + # <tt>person.tasks.each(&:destroy)</tt>. + # + # == Type safety with ActiveRecord::AssociationTypeMismatch + # + # If you attempt to assign an object to an association that doesn't match the inferred + # or specified <tt>:class_name</tt>, you'll get an ActiveRecord::AssociationTypeMismatch. + # + # == Options + # + # All of the association macros can be specialized through options. This makes cases + # more complex than the simple and guessable ones possible. + module ClassMethods + # Specifies a one-to-many association. The following methods for retrieval and query of + # collections of associated objects will be added: + # + # +collection+ is a placeholder for the symbol passed as the +name+ argument, so + # <tt>has_many :clients</tt> would add among others <tt>clients.empty?</tt>. + # + # [collection(force_reload = false)] + # Returns an array of all the associated objects. + # An empty array is returned if none are found. + # [collection<<(object, ...)] + # Adds one or more objects to the collection by setting their foreign keys to the collection's primary key. + # Note that this operation instantly fires update SQL without waiting for the save or update call on the + # parent object, unless the parent object is a new record. + # This will also run validations and callbacks of associated object(s). + # [collection.delete(object, ...)] + # Removes one or more objects from the collection by setting their foreign keys to +NULL+. + # Objects will be in addition destroyed if they're associated with <tt>dependent: :destroy</tt>, + # and deleted if they're associated with <tt>dependent: :delete_all</tt>. + # + # If the <tt>:through</tt> option is used, then the join records are deleted (rather than + # nullified) by default, but you can specify <tt>dependent: :destroy</tt> or + # <tt>dependent: :nullify</tt> to override this. + # [collection.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 + # direct by default. You can specify <tt>dependent: :destroy</tt> or + # <tt>dependent: :nullify</tt> to override this. + # [collection_singular_ids] + # Returns an array of the associated objects' ids + # [collection_singular_ids=ids] + # Replace the collection with the objects identified by the primary keys in +ids+. This + # 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+. + # If the <tt>:through</tt> option is true no destroy callbacks are invoked on the join models. + # Join models are directly deleted. + # [collection.empty?] + # Returns +true+ if there are no associated objects. + # [collection.size] + # Returns the number of associated objects. + # [collection.find(...)] + # Finds an associated object according to the same rules as ActiveRecord::FinderMethods#find. + # [collection.exists?(...)] + # Checks whether an associated object with the given conditions exists. + # Uses the same rules as ActiveRecord::FinderMethods#exists?. + # [collection.build(attributes = {}, ...)] + # Returns one or more new objects of the collection type that have been instantiated + # with +attributes+ and linked to this object through a foreign key, but have not yet + # been saved. + # [collection.create(attributes = {})] + # Returns a new object of the collection type that has been instantiated + # with +attributes+, linked to this object through a foreign key, and that has already + # been saved (if it passed the validation). *Note*: This only works if the base model + # already exists in the DB, not if it is a new (unsaved) record! + # [collection.create!(attributes = {})] + # Does the same as <tt>collection.create</tt>, but raises ActiveRecord::RecordInvalid + # if the record is invalid. + # + # === Example + # + # A <tt>Firm</tt> class declares <tt>has_many :clients</tt>, which will add: + # * <tt>Firm#clients</tt> (similar to <tt>Client.where(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.where(firm_id: id).find(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>) + # * <tt>Firm#clients.create!</tt> (similar to <tt>c = Client.new("firm_id" => id); c.save!</tt>) + # The declaration can also include an +options+ hash to specialize the behavior of the association. + # + # === Scopes + # + # You can pass a second argument +scope+ as a callable (i.e. proc or + # lambda) to retrieve a specific set of records or customize the generated + # query when you access the associated collection. + # + # Scope examples: + # has_many :comments, -> { where(author_id: 1) } + # has_many :employees, -> { joins(:address) } + # has_many :posts, ->(post) { where("max_post_length > ?", post.length) } + # + # === Extensions + # + # The +extension+ argument allows you to pass a block into a has_many + # association. This is useful for adding new finders, creators and other + # factory-type methods to be used as part of the association. + # + # Extension examples: + # has_many :employees do + # def find_or_create_by_name(name) + # first_name, last_name = name.split(" ", 2) + # find_or_create_by(first_name: first_name, last_name: last_name) + # end + # end + # + # === Options + # [:class_name] + # Specify the class name of the association. Use it only if that name can't be inferred + # from the association name. So <tt>has_many :products</tt> will by default be linked + # to the +Product+ class, but if the real class name is +SpecialProduct+, you'll have to + # specify it with this option. + # [:foreign_key] + # Specify the foreign key used for the association. By default this is guessed to be the name + # of this class in lower-case and "_id" suffixed. So a Person class that makes a #has_many + # association will use "person_id" as the default <tt>:foreign_key</tt>. + # [:foreign_type] + # Specify the column used to store the associated object's type, if this is a polymorphic + # association. By default this is guessed to be the name of the polymorphic association + # specified on "as" option with a "_type" suffix. So a class that defines a + # <tt>has_many :tags, as: :taggable</tt> association will use "taggable_type" as the + # default <tt>:foreign_type</tt>. + # [:primary_key] + # Specify the name of the column to use as the primary key for the association. By default this is +id+. + # [:dependent] + # Controls what happens to the associated objects when + # their owner is destroyed. Note that these are implemented as + # callbacks, and Rails executes callbacks in order. Therefore, other + # similar callbacks may affect the <tt>:dependent</tt> behavior, and the + # <tt>:dependent</tt> behavior may affect other callbacks. + # + # * <tt>:destroy</tt> causes all the associated objects to also be destroyed. + # * <tt>:delete_all</tt> causes all the associated objects to be deleted directly from the database (so callbacks will not be executed). + # * <tt>:nullify</tt> causes the foreign keys to be set to +NULL+. Callbacks are not executed. + # * <tt>:restrict_with_exception</tt> causes an exception to be raised if there are any associated records. + # * <tt>:restrict_with_error</tt> causes an error to be added to the owner if there are any associated objects. + # + # If using with the <tt>:through</tt> option, the association on the join model must be + # a #belongs_to, and the records which get deleted are the join records, rather than + # the associated records. + # + # If using <tt>dependent: :destroy</tt> on a scoped association, only the scoped objects are destroyed. + # For example, if a Post model defines + # <tt>has_many :comments, -> { where published: true }, dependent: :destroy</tt> and <tt>destroy</tt> is + # called on a post, only published comments are destroyed. This means that any unpublished comments in the + # database would still contain a foreign key pointing to the now deleted post. + # [: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 #belongs_to association. + # [:as] + # Specifies a polymorphic interface (See #belongs_to). + # [:through] + # Specifies an association through which to perform the query. This can be any other type + # of association, including other <tt>:through</tt> associations. Options for <tt>:class_name</tt>, + # <tt>:primary_key</tt> and <tt>:foreign_key</tt> are ignored, as the association uses the + # source reflection. + # + # If the association on the join model is a #belongs_to, the collection can be modified + # and the records on the <tt>:through</tt> model will be automatically created and removed + # as appropriate. Otherwise, the collection is read-only, so you should manipulate the + # <tt>:through</tt> association directly. + # + # If you are going to modify the association (rather than just read from it), then it is + # a good idea to set the <tt>:inverse_of</tt> option on the source association on the + # join model. This allows associated records to be built which will automatically create + # the appropriate join model records when they are saved. (See the 'Association Join Models' + # section above.) + # [:source] + # Specifies the source association name used by #has_many <tt>:through</tt> queries. + # Only use it if the name cannot be inferred from the association. + # <tt>has_many :subscribers, through: :subscriptions</tt> will look for either <tt>:subscribers</tt> or + # <tt>:subscriber</tt> on Subscription, unless a <tt>:source</tt> is given. + # [:source_type] + # Specifies type of the source association used by #has_many <tt>:through</tt> queries where the source + # association is a polymorphic #belongs_to. + # [:validate] + # When set to +true+, validates new objects added to association when saving the parent object. +true+ by default. + # If you want to ensure associated objects are revalidated on every update, use +validates_associated+. + # [:autosave] + # If true, always save the associated objects or destroy them if marked for destruction, + # when saving the parent object. If false, never save or destroy the associated objects. + # By default, only save associated objects that are new records. This option is implemented as a + # +before_save+ callback. Because callbacks are run in the order they are defined, associated objects + # may need to be explicitly saved in any user-defined +before_save+ callbacks. + # + # Note that NestedAttributes::ClassMethods#accepts_nested_attributes_for sets + # <tt>:autosave</tt> to <tt>true</tt>. + # [:inverse_of] + # Specifies the name of the #belongs_to association on the associated object + # that is the inverse of this #has_many association. Does not work in combination + # with <tt>:through</tt> or <tt>:as</tt> options. + # See ActiveRecord::Associations::ClassMethods's overview on Bi-directional associations for more detail. + # [:extend] + # Specifies a module or array of modules that will be extended into the association object returned. + # Useful for defining methods on associations, especially when they should be shared between multiple + # association objects. + # + # Option examples: + # has_many :comments, -> { order("posted_on") } + # has_many :comments, -> { includes(:author) } + # has_many :people, -> { where(deleted: false).order("name") }, class_name: "Person" + # has_many :tracks, -> { order("position") }, dependent: :destroy + # has_many :comments, dependent: :nullify + # has_many :tags, as: :taggable + # has_many :reports, -> { readonly } + # has_many :subscribers, through: :subscriptions, source: :user + def has_many(name, scope = nil, options = {}, &extension) + reflection = Builder::HasMany.build(self, name, scope, options, &extension) + Reflection.add_reflection self, name, reflection end - habtm_reflection = ActiveRecord::Reflection::HasAndBelongsToManyReflection.new(name, scope, options, self) + # Specifies a one-to-one association with another class. This method should only be used + # if the other class contains the foreign key. If the current class contains the foreign key, + # then you should use #belongs_to instead. See also ActiveRecord::Associations::ClassMethods's overview + # on when to use #has_one and when to use #belongs_to. + # + # The following methods for retrieval and query of a single associated object will be added: + # + # +association+ is a placeholder for the symbol passed as the +name+ argument, so + # <tt>has_one :manager</tt> would add among others <tt>manager.nil?</tt>. + # + # [association(force_reload = false)] + # Returns the associated object. +nil+ is returned if none is found. + # [association=(associate)] + # Assigns the associate object, extracts the primary key, sets it as the foreign key, + # and saves the associate object. To avoid database inconsistencies, permanently deletes an existing + # associated object when assigning a new one, even if the new one isn't saved to database. + # [build_association(attributes = {})] + # Returns a new object of the associated type that has been instantiated + # with +attributes+ and linked to this object through a foreign key, but has not + # yet been saved. + # [create_association(attributes = {})] + # Returns a new object of the associated type that has been instantiated + # with +attributes+, linked to this object through a foreign key, and that + # has already been saved (if it passed the validation). + # [create_association!(attributes = {})] + # Does the same as <tt>create_association</tt>, but raises ActiveRecord::RecordInvalid + # if the record is invalid. + # + # === Example + # + # An Account class declares <tt>has_one :beneficiary</tt>, which will add: + # * <tt>Account#beneficiary</tt> (similar to <tt>Beneficiary.where(account_id: id).first</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>) + # * <tt>Account#create_beneficiary!</tt> (similar to <tt>b = Beneficiary.new("account_id" => id); b.save!; b</tt>) + # + # === Scopes + # + # You can pass a second argument +scope+ as a callable (i.e. proc or + # lambda) to retrieve a specific record or customize the generated query + # when you access the associated object. + # + # Scope examples: + # has_one :author, -> { where(comment_id: 1) } + # has_one :employer, -> { joins(:company) } + # has_one :dob, ->(dob) { where("Date.new(2000, 01, 01) > ?", dob) } + # + # === Options + # + # The declaration can also include an +options+ hash to specialize the behavior of the association. + # + # Options are: + # [:class_name] + # Specify the class name of the association. Use it only if that name can't be inferred + # from the association name. So <tt>has_one :manager</tt> will by default be linked to the Manager class, but + # if the real class name is Person, you'll have to specify it with this option. + # [:dependent] + # Controls what happens to the associated object when + # its owner is destroyed: + # + # * <tt>:destroy</tt> causes the associated object to also be destroyed + # * <tt>:delete</tt> causes the associated object to be deleted directly from the database (so callbacks will not execute) + # * <tt>:nullify</tt> causes the foreign key to be set to +NULL+. Callbacks are not executed. + # * <tt>:restrict_with_exception</tt> causes an exception to be raised if there is an associated record + # * <tt>:restrict_with_error</tt> causes an error to be added to the owner if there is an associated object + # + # Note that <tt>:dependent</tt> option is ignored when using <tt>:through</tt> option. + # [:foreign_key] + # Specify the foreign key used for the association. By default this is guessed to be the name + # of this class in lower-case and "_id" suffixed. So a Person class that makes a #has_one association + # will use "person_id" as the default <tt>:foreign_key</tt>. + # [:foreign_type] + # Specify the column used to store the associated object's type, if this is a polymorphic + # association. By default this is guessed to be the name of the polymorphic association + # specified on "as" option with a "_type" suffix. So a class that defines a + # <tt>has_one :tag, as: :taggable</tt> association will use "taggable_type" as the + # default <tt>:foreign_type</tt>. + # [:primary_key] + # Specify the method that returns the primary key used for the association. By default this is +id+. + # [:as] + # Specifies a polymorphic interface (See #belongs_to). + # [:through] + # Specifies a Join Model through which to perform the query. Options for <tt>:class_name</tt>, + # <tt>:primary_key</tt>, and <tt>:foreign_key</tt> are ignored, as the association uses the + # source reflection. You can only use a <tt>:through</tt> query through a #has_one + # or #belongs_to association on the join model. + # [:source] + # Specifies the source association name used by #has_one <tt>:through</tt> queries. + # Only use it if the name cannot be inferred from the association. + # <tt>has_one :favorite, through: :favorites</tt> will look for a + # <tt>:favorite</tt> on Favorite, unless a <tt>:source</tt> is given. + # [:source_type] + # Specifies type of the source association used by #has_one <tt>:through</tt> queries where the source + # association is a polymorphic #belongs_to. + # [:validate] + # When set to +true+, validates new objects added to association when saving the parent object. +false+ by default. + # If you want to ensure associated objects are revalidated on every update, use +validates_associated+. + # [:autosave] + # If true, always save the associated object or destroy it if marked for destruction, + # when saving the parent object. If false, never save or destroy the associated object. + # By default, only save the associated object if it's a new record. + # + # Note that NestedAttributes::ClassMethods#accepts_nested_attributes_for sets + # <tt>:autosave</tt> to <tt>true</tt>. + # [:inverse_of] + # Specifies the name of the #belongs_to association on the associated object + # that is the inverse of this #has_one association. Does not work in combination + # with <tt>:through</tt> or <tt>:as</tt> options. + # See ActiveRecord::Associations::ClassMethods's overview on Bi-directional associations for more detail. + # [:required] + # When set to +true+, the association will also have its presence validated. + # This will validate the association itself, not the id. You can use + # +:inverse_of+ to avoid an extra query during validation. + # + # Option examples: + # 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 :attachment, as: :attachable + # has_one :boss, -> { readonly } + # has_one :club, through: :membership + # has_one :primary_address, -> { where(primary: true) }, through: :addressables, source: :addressable + # has_one :credit_card, required: true + def has_one(name, scope = nil, options = {}) + reflection = Builder::HasOne.build(self, name, scope, options) + Reflection.add_reflection self, name, reflection + end - builder = Builder::HasAndBelongsToMany.new name, self, options + # Specifies a one-to-one association with another class. This method should only be used + # if this class contains the foreign key. If the other class contains the foreign key, + # then you should use #has_one instead. See also ActiveRecord::Associations::ClassMethods's overview + # on when to use #has_one and when to use #belongs_to. + # + # Methods will be added for retrieval and query for a single associated object, for which + # this object holds an id: + # + # +association+ is a placeholder for the symbol passed as the +name+ argument, so + # <tt>belongs_to :author</tt> would add among others <tt>author.nil?</tt>. + # + # [association(force_reload = false)] + # Returns the associated object. +nil+ is returned if none is found. + # [association=(associate)] + # Assigns the associate object, extracts the primary key, and sets it as the foreign key. + # [build_association(attributes = {})] + # Returns a new object of the associated type that has been instantiated + # with +attributes+ and linked to this object through a foreign key, but has not yet been saved. + # [create_association(attributes = {})] + # Returns a new object of the associated type that has been instantiated + # with +attributes+, linked to this object through a foreign key, and that + # has already been saved (if it passed the validation). + # [create_association!(attributes = {})] + # Does the same as <tt>create_association</tt>, but raises ActiveRecord::RecordInvalid + # if the record is invalid. + # + # === Example + # + # A Post class declares <tt>belongs_to :author</tt>, which will add: + # * <tt>Post#author</tt> (similar to <tt>Author.find(author_id)</tt>) + # * <tt>Post#author=(author)</tt> (similar to <tt>post.author_id = author.id</tt>) + # * <tt>Post#build_author</tt> (similar to <tt>post.author = Author.new</tt>) + # * <tt>Post#create_author</tt> (similar to <tt>post.author = Author.new; post.author.save; post.author</tt>) + # * <tt>Post#create_author!</tt> (similar to <tt>post.author = Author.new; post.author.save!; post.author</tt>) + # The declaration can also include an +options+ hash to specialize the behavior of the association. + # + # === Scopes + # + # You can pass a second argument +scope+ as a callable (i.e. proc or + # lambda) to retrieve a specific record or customize the generated query + # when you access the associated object. + # + # Scope examples: + # belongs_to :firm, -> { where(id: 2) } + # belongs_to :user, -> { joins(:friends) } + # belongs_to :level, ->(level) { where("game_level > ?", level.current) } + # + # === Options + # + # [:class_name] + # Specify the class name of the association. Use it only if that name can't be inferred + # from the association name. So <tt>belongs_to :author</tt> will by default be linked to the Author class, but + # if the real class name is Person, you'll have to specify it with this option. + # [:foreign_key] + # Specify the foreign key used for the association. By default this is guessed to be the name + # of the association with an "_id" suffix. So a class that defines a <tt>belongs_to :person</tt> + # association will use "person_id" as the default <tt>:foreign_key</tt>. Similarly, + # <tt>belongs_to :favorite_person, class_name: "Person"</tt> will use a foreign key + # of "favorite_person_id". + # [:foreign_type] + # Specify the column used to store the associated object's type, if this is a polymorphic + # association. By default this is guessed to be the name of the association with a "_type" + # suffix. So a class that defines a <tt>belongs_to :taggable, polymorphic: true</tt> + # association will use "taggable_type" as the default <tt>:foreign_type</tt>. + # [:primary_key] + # Specify the method that returns the primary key of associated object used for the association. + # By default this is id. + # [:dependent] + # If set to <tt>:destroy</tt>, the associated object is destroyed when this object is. If set to + # <tt>:delete</tt>, the associated object is deleted *without* calling its destroy method. + # This option should not be specified when #belongs_to is used in conjunction with + # a #has_many relationship on another class because of the potential to leave + # orphaned records behind. + # [:counter_cache] + # Caches the number of belonging objects on the associate class through the use of CounterCache::ClassMethods#increment_counter + # and CounterCache::ClassMethods#decrement_counter. The counter cache is incremented when an object of this + # class is created and decremented when it's destroyed. This requires that a column + # named <tt>#{table_name}_count</tt> (such as +comments_count+ for a belonging Comment class) + # is used on the associate class (such as a Post class) - that is the migration for + # <tt>#{table_name}_count</tt> is created on the associate class (such that <tt>Post.comments_count</tt> will + # return the count cached, see note below). You can also specify a custom counter + # cache column by providing a column name instead of a +true+/+false+ value to this + # option (e.g., <tt>counter_cache: :my_custom_counter</tt>.) + # Note: Specifying a counter cache will add it to that model's list of readonly attributes + # using +attr_readonly+. + # [:polymorphic] + # Specify this association is a polymorphic association by passing +true+. + # Note: If you've enabled the counter cache, then you may want to add the counter cache attribute + # to the +attr_readonly+ list in the associated classes (e.g. <tt>class Post; attr_readonly :comments_count; end</tt>). + # [:validate] + # When set to +true+, validates new objects added to association when saving the parent object. +false+ by default. + # If you want to ensure associated objects are revalidated on every update, use +validates_associated+. + # [:autosave] + # If true, always save the associated object or destroy it if marked for destruction, when + # saving the parent object. + # If false, never save or destroy the associated object. + # By default, only save the associated object if it's a new record. + # + # Note that NestedAttributes::ClassMethods#accepts_nested_attributes_for + # sets <tt>:autosave</tt> to <tt>true</tt>. + # [:touch] + # If true, the associated object will be touched (the updated_at/on attributes set to current time) + # when this record is either saved or destroyed. If you specify a symbol, that attribute + # will be updated with the current time in addition to the updated_at/on attribute. + # Please note that with touching no validation is performed and only the +after_touch+, + # +after_commit+ and +after_rollback+ callbacks are executed. + # [:inverse_of] + # Specifies the name of the #has_one or #has_many association on the associated + # object that is the inverse of this #belongs_to association. Does not work in + # combination with the <tt>:polymorphic</tt> options. + # See ActiveRecord::Associations::ClassMethods's overview on Bi-directional associations for more detail. + # [:optional] + # When set to +true+, the association will not have its presence validated. + # [:required] + # When set to +true+, the association will also have its presence validated. + # This will validate the association itself, not the id. You can use + # +:inverse_of+ to avoid an extra query during validation. + # NOTE: <tt>required</tt> is set to <tt>true</tt> by default and is deprecated. If + # you don't want to have association presence validated, use <tt>optional: true</tt>. + # + # Option examples: + # belongs_to :firm, foreign_key: "client_of" + # belongs_to :person, primary_key: "name", foreign_key: "person_name" + # belongs_to :author, class_name: "Person", foreign_key: "author_id" + # belongs_to :valid_coupon, ->(o) { where "discounts > ?", o.payments_count }, + # class_name: "Coupon", foreign_key: "coupon_id" + # belongs_to :attachable, polymorphic: true + # belongs_to :project, -> { readonly } + # belongs_to :post, counter_cache: true + # belongs_to :comment, touch: true + # belongs_to :company, touch: :employees_last_updated_at + # belongs_to :user, optional: true + def belongs_to(name, scope = nil, options = {}) + reflection = Builder::BelongsTo.build(self, name, scope, options) + Reflection.add_reflection self, name, reflection + end - join_model = builder.through_model + # Specifies a many-to-many relationship with another class. This associates two classes via an + # intermediate join table. Unless the join table is explicitly specified as an option, it is + # guessed using the lexical order of the class names. So a join between Developer and Project + # will give the default join table name of "developers_projects" because "D" precedes "P" alphabetically. + # Note that this precedence is calculated using the <tt><</tt> operator for String. This + # means that if the strings are of different lengths, and the strings are equal when compared + # up to the shortest length, then the longer string is considered of higher + # lexical precedence than the shorter one. For example, one would expect the tables "paper_boxes" and "papers" + # to generate a join table name of "papers_paper_boxes" because of the length of the name "paper_boxes", + # but it in fact generates a join table name of "paper_boxes_papers". Be aware of this caveat, and use the + # custom <tt>:join_table</tt> option if you need to. + # If your tables share a common prefix, it will only appear once at the beginning. For example, + # the tables "catalog_categories" and "catalog_products" generate a join table name of "catalog_categories_products". + # + # The join table should not have a primary key or a model associated with it. You must manually generate the + # join table with a migration such as this: + # + # class CreateDevelopersProjectsJoinTable < ActiveRecord::Migration[5.0] + # def change + # create_join_table :developers, :projects + # end + # end + # + # It's also a good idea to add indexes to each of those columns to speed up the joins process. + # However, in MySQL it is advised to add a compound index for both of the columns as MySQL only + # uses one index per table during the lookup. + # + # Adds the following methods for retrieval and query: + # + # +collection+ is a placeholder for the symbol passed as the +name+ argument, so + # <tt>has_and_belongs_to_many :categories</tt> would add among others <tt>categories.empty?</tt>. + # + # [collection(force_reload = false)] + # Returns an array of all the associated objects. + # An empty array is returned if none are found. + # [collection<<(object, ...)] + # Adds one or more objects to the collection by creating associations in the join table + # (<tt>collection.push</tt> and <tt>collection.concat</tt> are aliases to this method). + # Note that this operation instantly fires update SQL without waiting for the save or update call on the + # parent object, unless the parent object is a new record. + # [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] + # Returns an array of the associated objects' ids. + # [collection_singular_ids=ids] + # Replace the collection by the objects identified by the primary keys in +ids+. + # [collection.clear] + # Removes every object from the collection. This does not destroy the objects. + # [collection.empty?] + # Returns +true+ if there are no associated objects. + # [collection.size] + # Returns the number of associated objects. + # [collection.find(id)] + # Finds an associated object responding to the +id+ and that + # meets the condition that it has to be associated with this object. + # Uses the same rules as ActiveRecord::FinderMethods#find. + # [collection.exists?(...)] + # Checks whether an associated object with the given conditions exists. + # Uses the same rules as ActiveRecord::FinderMethods#exists?. + # [collection.build(attributes = {})] + # Returns a new object of the collection type that has been instantiated + # with +attributes+ and linked to this object through the join table, but has not yet been saved. + # [collection.create(attributes = {})] + # Returns a new object of the collection type that has been instantiated + # with +attributes+, linked to this object through the join table, and that has already been + # saved (if it passed the validation). + # + # === Example + # + # A Developer class declares <tt>has_and_belongs_to_many :projects</tt>, which will add: + # * <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> + # * <tt>Developer#projects.clear</tt> + # * <tt>Developer#projects.empty?</tt> + # * <tt>Developer#projects.size</tt> + # * <tt>Developer#projects.find(id)</tt> + # * <tt>Developer#projects.exists?(...)</tt> + # * <tt>Developer#projects.build</tt> (similar to <tt>Project.new("developer_id" => id)</tt>) + # * <tt>Developer#projects.create</tt> (similar to <tt>c = Project.new("developer_id" => id); c.save; c</tt>) + # The declaration may include an +options+ hash to specialize the behavior of the association. + # + # === Scopes + # + # You can pass a second argument +scope+ as a callable (i.e. proc or + # lambda) to retrieve a specific set of records or customize the generated + # query when you access the associated collection. + # + # Scope examples: + # has_and_belongs_to_many :projects, -> { includes(:milestones, :manager) } + # has_and_belongs_to_many :categories, ->(category) { + # where("default_category = ?", category.name) + # } + # + # === Extensions + # + # The +extension+ argument allows you to pass a block into a + # has_and_belongs_to_many association. This is useful for adding new + # finders, creators and other factory-type methods to be used as part of + # the association. + # + # Extension examples: + # has_and_belongs_to_many :contractors do + # def find_or_create_by_name(name) + # first_name, last_name = name.split(" ", 2) + # find_or_create_by(first_name: first_name, last_name: last_name) + # end + # end + # + # === Options + # + # [:class_name] + # Specify the class name of the association. Use it only if that name can't be inferred + # from the association name. So <tt>has_and_belongs_to_many :projects</tt> will by default be linked to the + # Project class, but if the real class name is SuperProject, you'll have to specify it with this option. + # [:join_table] + # Specify the name of the join table if the default based on lexical order isn't what you want. + # <b>WARNING:</b> If you're overwriting the table name of either class, the +table_name+ method + # MUST be declared underneath any #has_and_belongs_to_many declaration in order to work. + # [:foreign_key] + # Specify the foreign key used for the association. By default this is guessed to be the name + # of this class in lower-case and "_id" suffixed. So a Person class that makes + # a #has_and_belongs_to_many association to Project will use "person_id" as the + # default <tt>:foreign_key</tt>. + # [:association_foreign_key] + # Specify the foreign key used for the association on the receiving side of the association. + # By default this is guessed to be the name of the associated class in lower-case and "_id" suffixed. + # So if a Person class makes a #has_and_belongs_to_many association to Project, + # the association will use "project_id" as the default <tt>:association_foreign_key</tt>. + # [:validate] + # When set to +true+, validates new objects added to association when saving the parent object. +true+ by default. + # If you want to ensure associated objects are revalidated on every update, use +validates_associated+. + # [:autosave] + # If true, always save the associated objects or destroy them if marked for destruction, when + # saving the parent object. + # If false, never save or destroy the associated objects. + # By default, only save associated objects that are new records. + # + # Note that NestedAttributes::ClassMethods#accepts_nested_attributes_for sets + # <tt>:autosave</tt> to <tt>true</tt>. + # + # Option examples: + # has_and_belongs_to_many :projects + # has_and_belongs_to_many :projects, -> { includes(:milestones, :manager) } + # has_and_belongs_to_many :nations, class_name: "Country" + # has_and_belongs_to_many :categories, join_table: "prods_cats" + # has_and_belongs_to_many :categories, -> { readonly } + def has_and_belongs_to_many(name, scope = nil, **options, &extension) + habtm_reflection = ActiveRecord::Reflection::HasAndBelongsToManyReflection.new(name, scope, options, self) - const_set join_model.name, join_model - private_constant join_model.name + builder = Builder::HasAndBelongsToMany.new name, self, options - middle_reflection = builder.middle_reflection join_model + join_model = builder.through_model - Builder::HasMany.define_callbacks self, middle_reflection - Reflection.add_reflection self, middle_reflection.name, middle_reflection - middle_reflection.parent_reflection = habtm_reflection + const_set join_model.name, join_model + private_constant join_model.name - include Module.new { - class_eval <<-RUBY, __FILE__, __LINE__ + 1 - def destroy_associations - association(:#{middle_reflection.name}).delete_all(:delete_all) - association(:#{name}).reset - super - end - RUBY - } + middle_reflection = builder.middle_reflection join_model - hm_options = {} - hm_options[:through] = middle_reflection.name - hm_options[:source] = join_model.right_reflection.name + Builder::HasMany.define_callbacks self, middle_reflection + Reflection.add_reflection self, middle_reflection.name, middle_reflection + middle_reflection.parent_reflection = habtm_reflection - [:before_add, :after_add, :before_remove, :after_remove, :autosave, :validate, :join_table, :class_name, :extend].each do |k| - hm_options[k] = options[k] if options.key? k - end + include Module.new { + class_eval <<-RUBY, __FILE__, __LINE__ + 1 + def destroy_associations + association(:#{middle_reflection.name}).delete_all(:delete_all) + association(:#{name}).reset + super + end + RUBY + } - has_many name, scope, hm_options, &extension - self._reflections[name.to_s].parent_reflection = habtm_reflection + hm_options = {} + hm_options[:through] = middle_reflection.name + hm_options[:source] = join_model.right_reflection.name + + [:before_add, :after_add, :before_remove, :after_remove, :autosave, :validate, :join_table, :class_name, :extend].each do |k| + hm_options[k] = options[k] if options.key? k + end + + has_many name, scope, hm_options, &extension + self._reflections[name.to_s].parent_reflection = habtm_reflection + end end - end end end diff --git a/activerecord/lib/active_record/associations/alias_tracker.rb b/activerecord/lib/active_record/associations/alias_tracker.rb index 2b7e4f28c5..3963008a76 100644 --- a/activerecord/lib/active_record/associations/alias_tracker.rb +++ b/activerecord/lib/active_record/associations/alias_tracker.rb @@ -1,9 +1,8 @@ -require 'active_support/core_ext/string/conversions' +require "active_support/core_ext/string/conversions" module ActiveRecord module Associations - # Keeps track of table aliases for ActiveRecord::Associations::ClassMethods::JoinDependency and - # ActiveRecord::Associations::ThroughAssociationScope + # Keeps track of table aliases for ActiveRecord::Associations::JoinDependency class AliasTracker # :nodoc: attr_reader :aliases diff --git a/activerecord/lib/active_record/associations/association.rb b/activerecord/lib/active_record/associations/association.rb index 7c729676a7..84d0493a60 100644 --- a/activerecord/lib/active_record/associations/association.rb +++ b/activerecord/lib/active_record/associations/association.rb @@ -1,4 +1,4 @@ -require 'active_support/core_ext/array/wrap' +require "active_support/core_ext/array/wrap" module ActiveRecord module Associations @@ -19,7 +19,7 @@ module ActiveRecord attr_reader :owner, :target, :reflection attr_accessor :inversed - delegate :options, :to => :reflection + delegate :options, to: :reflection def initialize(owner, reflection) reflection.check_validity! @@ -112,6 +112,15 @@ module ActiveRecord record end + # Remove the inverse association, if possible + def remove_inverse_instance(record) + if invertible_for?(record) + inverse = record.association(inverse_reflection_for(record).name) + inverse.target = nil + inverse.inversed = false + end + end + # Returns the class of the target. belongs_to polymorphic overrides this to look at the # polymorphic_type field on the owner. def klass @@ -163,13 +172,24 @@ module ActiveRecord @reflection = @owner.class._reflect_on_association(reflection_name) end - def initialize_attributes(record) #:nodoc: + def initialize_attributes(record, except_from_scope_attributes = nil) #:nodoc: + except_from_scope_attributes ||= {} skip_assign = [reflection.foreign_key, reflection.type].compact - attributes = create_scope.except(*(record.changed - skip_assign)) + assigned_keys = record.changed_attribute_names_to_save + assigned_keys += except_from_scope_attributes.keys.map(&:to_s) + attributes = create_scope.except(*(assigned_keys - skip_assign)) record.assign_attributes(attributes) set_inverse_instance(record) end + def create(attributes = {}, &block) + _create_record(attributes, &block) + end + + def create!(attributes = {}, &block) + _create_record(attributes, true, &block) + end + private def find_target? @@ -214,7 +234,8 @@ module ActiveRecord unless record.is_a?(reflection.klass) fresh_class = reflection.class_name.safe_constantize unless fresh_class && record.is_a?(fresh_class) - message = "#{reflection.class_name}(##{reflection.klass.object_id}) expected, got #{record.class}(##{record.class.object_id})" + message = "#{reflection.class_name}(##{reflection.klass.object_id}) expected, "\ + "got #{record.inspect} which is an instance of #{record.class}(##{record.class.object_id})" raise ActiveRecord::AssociationTypeMismatch, message end end @@ -242,15 +263,23 @@ module ActiveRecord # 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. + # This is only relevant to certain associations, which is why it returns +nil+ by default. def stale_state end def build_record(attributes) reflection.build_association(attributes) do |record| - initialize_attributes(record) + initialize_attributes(record, attributes) end end + + # Returns true if statement cache should be skipped on the association reader. + def skip_statement_cache? + reflection.has_scope? || + scope.eager_loading? || + klass.scope_attributes? || + reflection.source_reflection.active_record.default_scopes.any? + end end end end diff --git a/activerecord/lib/active_record/associations/association_scope.rb b/activerecord/lib/active_record/associations/association_scope.rb index a140dc239c..c6d204d3c2 100644 --- a/activerecord/lib/active_record/associations/association_scope.rb +++ b/activerecord/lib/active_record/associations/association_scope.rb @@ -49,118 +49,119 @@ module ActiveRecord binds end + # TODO Change this to private once we've dropped Ruby 2.2 support. + # Workaround for Ruby 2.2 "private attribute?" warning. protected - attr_reader :value_transformation + attr_reader :value_transformation private - def join(table, constraint) - table.create_join(table, table.create_on(constraint), join_type) - end + def join(table, constraint) + table.create_join(table, table.create_on(constraint), join_type) + end + + def last_chain_scope(scope, table, reflection, owner, association_klass) + join_keys = reflection.join_keys(association_klass) + key = join_keys.key + foreign_key = join_keys.foreign_key - def last_chain_scope(scope, table, reflection, owner, association_klass) - join_keys = reflection.join_keys(association_klass) - key = join_keys.key - foreign_key = join_keys.foreign_key + value = transform_value(owner[foreign_key]) + scope = scope.where(table.name => { key => value }) - value = transform_value(owner[foreign_key]) - scope = scope.where(table.name => { key => value }) + if reflection.type + polymorphic_type = transform_value(owner.class.base_class.name) + scope = scope.where(table.name => { reflection.type => polymorphic_type }) + end - if reflection.type - polymorphic_type = transform_value(owner.class.base_class.name) - scope = scope.where(table.name => { reflection.type => polymorphic_type }) + scope end - scope - end + def transform_value(value) + value_transformation.call(value) + end - def transform_value(value) - value_transformation.call(value) - end + def next_chain_scope(scope, table, reflection, association_klass, foreign_table, next_reflection) + join_keys = reflection.join_keys(association_klass) + key = join_keys.key + foreign_key = join_keys.foreign_key - def next_chain_scope(scope, table, reflection, association_klass, foreign_table, next_reflection) - join_keys = reflection.join_keys(association_klass) - key = join_keys.key - foreign_key = join_keys.foreign_key + constraint = table[key].eq(foreign_table[foreign_key]) - constraint = table[key].eq(foreign_table[foreign_key]) + if reflection.type + value = transform_value(next_reflection.klass.base_class.name) + scope = scope.where(table.name => { reflection.type => value }) + end - if reflection.type - value = transform_value(next_reflection.klass.base_class.name) - scope = scope.where(table.name => { reflection.type => value }) + scope = scope.joins(join(foreign_table, constraint)) end - scope = scope.joins(join(foreign_table, constraint)) - end + class ReflectionProxy < SimpleDelegator # :nodoc: + attr_accessor :next + attr_reader :alias_name - class ReflectionProxy < SimpleDelegator # :nodoc: - attr_accessor :next - attr_reader :alias_name + def initialize(reflection, alias_name) + super(reflection) + @alias_name = alias_name + end - def initialize(reflection, alias_name) - super(reflection) - @alias_name = alias_name + def all_includes; nil; end end - def all_includes; nil; end - end - - def get_chain(reflection, association, tracker) - name = reflection.name - runtime_reflection = Reflection::RuntimeReflection.new(reflection, association) - previous_reflection = runtime_reflection - reflection.chain.drop(1).each do |refl| - alias_name = tracker.aliased_table_for(refl.table_name, refl.alias_candidate(name)) - proxy = ReflectionProxy.new(refl, alias_name) - previous_reflection.next = proxy - previous_reflection = proxy + def get_chain(reflection, association, tracker) + name = reflection.name + runtime_reflection = Reflection::RuntimeReflection.new(reflection, association) + previous_reflection = runtime_reflection + reflection.chain.drop(1).each do |refl| + alias_name = tracker.aliased_table_for(refl.table_name, refl.alias_candidate(name)) + proxy = ReflectionProxy.new(refl, alias_name) + previous_reflection.next = proxy + previous_reflection = proxy + end + [runtime_reflection, previous_reflection] end - [runtime_reflection, previous_reflection] - end - def add_constraints(scope, owner, association_klass, refl, chain_head, chain_tail) - owner_reflection = chain_tail - table = owner_reflection.alias_name - scope = last_chain_scope(scope, table, owner_reflection, owner, association_klass) + def add_constraints(scope, owner, association_klass, refl, chain_head, chain_tail) + owner_reflection = chain_tail + table = owner_reflection.alias_name + scope = last_chain_scope(scope, table, owner_reflection, owner, association_klass) - reflection = chain_head - loop do - break unless reflection - table = reflection.alias_name + reflection = chain_head + while reflection + table = reflection.alias_name - unless reflection == chain_tail - next_reflection = reflection.next - foreign_table = next_reflection.alias_name - scope = next_chain_scope(scope, table, reflection, association_klass, foreign_table, next_reflection) - end + unless reflection == chain_tail + next_reflection = reflection.next + foreign_table = next_reflection.alias_name + scope = next_chain_scope(scope, table, reflection, association_klass, foreign_table, next_reflection) + end - # Exclude the scope of the association itself, because that - # was already merged in the #scope method. - reflection.constraints.each do |scope_chain_item| - item = eval_scope(reflection.klass, scope_chain_item, owner) + # Exclude the scope of the association itself, because that + # was already merged in the #scope method. + reflection.constraints.each do |scope_chain_item| + item = eval_scope(reflection.klass, scope_chain_item, owner) - if scope_chain_item == refl.scope - scope.merge! item.except(:where, :includes) - end + if scope_chain_item == refl.scope + scope.merge! item.except(:where, :includes) + end + + reflection.all_includes do + scope.includes! item.includes_values + end - reflection.all_includes do - scope.includes! item.includes_values + scope.unscope!(*item.unscope_values) + scope.where_clause += item.where_clause + scope.order_values |= item.order_values end - scope.where_clause += item.where_clause - scope.order_values |= item.order_values - scope.unscope!(*item.unscope_values) + reflection = reflection.next end - reflection = reflection.next + scope end - scope - end - - def eval_scope(klass, scope, owner) - klass.unscoped.instance_exec(owner, &scope) - end + def eval_scope(klass, scope, owner) + klass.unscoped.instance_exec(owner, &scope) + end end end end diff --git a/activerecord/lib/active_record/associations/belongs_to_association.rb b/activerecord/lib/active_record/associations/belongs_to_association.rb index 260a0c6a2d..64b2311911 100644 --- a/activerecord/lib/active_record/associations/belongs_to_association.rb +++ b/activerecord/lib/active_record/associations/belongs_to_association.rb @@ -2,7 +2,6 @@ module ActiveRecord # = Active Record Belongs To Association module Associations class BelongsToAssociation < SingularAssociation #:nodoc: - def handle_dependency target.send(options[:dependent]) if load_target end @@ -10,7 +9,7 @@ module ActiveRecord def replace(record) if record raise_on_type_mismatch!(record) - update_counters(record) + update_counters_on_replace(record) replace_keys(record) set_inverse_instance(record) @updated = true @@ -32,45 +31,38 @@ module ActiveRecord end def decrement_counters # :nodoc: - with_cache_name { |name| decrement_counter name } + update_counters(-1) end def increment_counters # :nodoc: - with_cache_name { |name| increment_counter name } + update_counters(1) end private - def find_target? - !loaded? && foreign_key_present? && klass - end - - def with_cache_name - counter_cache_name = reflection.counter_cache_column - return unless counter_cache_name && owner.persisted? - yield counter_cache_name + def update_counters(by) + if require_counter_update? && foreign_key_present? + if target && !stale_target? + target.increment!(reflection.counter_cache_column, by) + else + klass.update_counters(target_id, reflection.counter_cache_column => by) + end + end end - def update_counters(record) - with_cache_name do |name| - return unless different_target? record - record.class.increment_counter(name, record.id) - decrement_counter name - end + def find_target? + !loaded? && foreign_key_present? && klass end - def decrement_counter(counter_cache_name) - if foreign_key_present? - klass.decrement_counter(counter_cache_name, target_id) - end + def require_counter_update? + reflection.counter_cache_column && owner.persisted? end - def increment_counter(counter_cache_name) - if foreign_key_present? - klass.increment_counter(counter_cache_name, target_id) - if target && !stale_target? - target.increment(counter_cache_name) - end + def update_counters_on_replace(record) + if require_counter_update? && different_target?(record) + owner.instance_variable_set :@_after_replace_counter_called, true + record.increment!(reflection.counter_cache_column) + decrement_counters end end diff --git a/activerecord/lib/active_record/associations/builder/association.rb b/activerecord/lib/active_record/associations/builder/association.rb index ba1b1814d1..d0534056d9 100644 --- a/activerecord/lib/active_record/associations/builder/association.rb +++ b/activerecord/lib/active_record/associations/builder/association.rb @@ -9,7 +9,7 @@ # - CollectionAssociation # - HasManyAssociation -module ActiveRecord::Associations::Builder +module ActiveRecord::Associations::Builder # :nodoc: class Association #:nodoc: class << self attr_accessor :extensions diff --git a/activerecord/lib/active_record/associations/builder/belongs_to.rb b/activerecord/lib/active_record/associations/builder/belongs_to.rb index 97eb007f62..a1609ab0fb 100644 --- a/activerecord/lib/active_record/associations/builder/belongs_to.rb +++ b/activerecord/lib/active_record/associations/builder/belongs_to.rb @@ -1,11 +1,11 @@ -module ActiveRecord::Associations::Builder +module ActiveRecord::Associations::Builder # :nodoc: class BelongsTo < SingularAssociation #:nodoc: def self.macro :belongs_to end def self.valid_options(options) - super + [:foreign_type, :polymorphic, :touch, :counter_cache, :optional] + super + [:polymorphic, :touch, :counter_cache, :optional] end def self.valid_dependent_options @@ -33,16 +33,26 @@ module ActiveRecord::Associations::Builder if (@_after_create_counter_called ||= false) @_after_create_counter_called = false - elsif attribute_changed?(foreign_key) && !new_record? && reflection.constructable? - model = reflection.klass - foreign_key_was = attribute_was foreign_key - foreign_key = attribute foreign_key + elsif (@_after_replace_counter_called ||= false) + @_after_replace_counter_called = false + elsif saved_change_to_attribute?(foreign_key) && !new_record? + if reflection.polymorphic? + model = attribute_in_database(reflection.foreign_type).try(:constantize) + model_was = attribute_before_last_save(reflection.foreign_type).try(:constantize) + else + model = reflection.klass + model_was = reflection.klass + end + + foreign_key_was = attribute_before_last_save foreign_key + foreign_key = attribute_in_database foreign_key if foreign_key && model.respond_to?(:increment_counter) model.increment_counter(cache_column, foreign_key) end - if foreign_key_was && model.respond_to?(:decrement_counter) - model.decrement_counter(cache_column, foreign_key_was) + + if foreign_key_was && model_was.respond_to?(:decrement_counter) + model_was.decrement_counter(cache_column, foreign_key_was) end end end @@ -60,14 +70,16 @@ module ActiveRecord::Associations::Builder klass.attr_readonly cache_column if klass && klass.respond_to?(:attr_readonly) end - def self.touch_record(o, foreign_key, name, touch, touch_method) # :nodoc: - old_foreign_id = o.changed_attributes[foreign_key] + def self.touch_record(o, changes, foreign_key, name, touch, touch_method) # :nodoc: + old_foreign_id = changes[foreign_key] && changes[foreign_key].first if old_foreign_id association = o.association(name) reflection = association.reflection if reflection.polymorphic? - klass = o.public_send("#{reflection.foreign_type}_was").constantize + foreign_type = reflection.foreign_type + klass = changes[foreign_type] && changes[foreign_type].first || o.public_send(foreign_type) + klass = klass.constantize else klass = association.klass end @@ -97,19 +109,17 @@ module ActiveRecord::Associations::Builder n = reflection.name touch = reflection.options[:touch] - callback = lambda { |record| - touch_method = touching_delayed_records? ? :touch : :touch_later - BelongsTo.touch_record(record, foreign_key, n, touch, touch_method) - } + callback = lambda { |changes_method| lambda { |record| + BelongsTo.touch_record(record, record.send(changes_method), foreign_key, n, touch, belongs_to_touch_method) + }} - model.after_save callback, if: :changed? - model.after_touch callback - model.after_destroy callback + model.after_save callback.(:saved_changes), if: :saved_changes? + model.after_touch callback.(:changes_to_save) + model.after_destroy callback.(:changes_to_save) end def self.add_destroy_callbacks(model, reflection) - name = reflection.name - model.after_destroy lambda { |o| o.association(name).handle_dependency } + model.after_destroy lambda { |o| o.association(reflection.name).handle_dependency } end def self.define_validations(model, reflection) diff --git a/activerecord/lib/active_record/associations/builder/collection_association.rb b/activerecord/lib/active_record/associations/builder/collection_association.rb index 2ff67f904d..edeb6491bd 100644 --- a/activerecord/lib/active_record/associations/builder/collection_association.rb +++ b/activerecord/lib/active_record/associations/builder/collection_association.rb @@ -1,10 +1,9 @@ # This class is inherited by the has_many and has_many_and_belongs_to_many association classes -require 'active_record/associations' +require "active_record/associations" -module ActiveRecord::Associations::Builder +module ActiveRecord::Associations::Builder # :nodoc: class CollectionAssociation < Association #:nodoc: - CALLBACKS = [:before_add, :after_add, :before_remove, :after_remove] def self.valid_options(options) @@ -70,7 +69,11 @@ module ActiveRecord::Associations::Builder def self.wrap_scope(scope, mod) if scope - proc { |owner| instance_exec(owner, &scope).extending(mod) } + if scope.arity > 0 + proc { |owner| instance_exec(owner, &scope).extending(mod) } + else + proc { instance_exec(&scope).extending(mod) } + end else proc { extending(mod) } end diff --git a/activerecord/lib/active_record/associations/builder/has_and_belongs_to_many.rb b/activerecord/lib/active_record/associations/builder/has_and_belongs_to_many.rb index ffd9c9d6fc..6b71826431 100644 --- a/activerecord/lib/active_record/associations/builder/has_and_belongs_to_many.rb +++ b/activerecord/lib/active_record/associations/builder/has_and_belongs_to_many.rb @@ -1,9 +1,9 @@ -module ActiveRecord::Associations::Builder +module ActiveRecord::Associations::Builder # :nodoc: class HasAndBelongsToMany # :nodoc: - class JoinTableResolver + class JoinTableResolver # :nodoc: KnownTable = Struct.new :join_table - class KnownClass + class KnownClass # :nodoc: def initialize(lhs_class, rhs_class_name) @lhs_class = lhs_class @rhs_class_name = rhs_class_name @@ -16,9 +16,9 @@ module ActiveRecord::Associations::Builder private - def klass - @lhs_class.send(:compute_type, @rhs_class_name) - end + def klass + @lhs_class.send(:compute_type, @rhs_class_name) + end end def self.build(lhs_class, name, options) @@ -28,7 +28,7 @@ module ActiveRecord::Associations::Builder class_name = options.fetch(:class_name) { name.to_s.camelize.singularize } - KnownClass.new lhs_class, class_name + KnownClass.new lhs_class, class_name.to_s end end end @@ -46,7 +46,7 @@ module ActiveRecord::Associations::Builder join_model = Class.new(ActiveRecord::Base) { class << self; - attr_accessor :class_resolver + attr_accessor :left_model attr_accessor :name attr_accessor :table_name_resolver attr_accessor :left_reflection @@ -58,25 +58,34 @@ module ActiveRecord::Associations::Builder end def self.compute_type(class_name) - class_resolver.compute_type class_name + left_model.compute_type class_name end def self.add_left_association(name, options) - belongs_to name, options + belongs_to name, required: false, **options self.left_reflection = _reflect_on_association(name) end def self.add_right_association(name, options) rhs_name = name.to_s.singularize.to_sym - belongs_to rhs_name, options + belongs_to rhs_name, required: false, **options self.right_reflection = _reflect_on_association(rhs_name) end + def self.retrieve_connection + left_model.retrieve_connection + end + + private + + def self.suppress_composite_primary_key(pk) + pk unless pk.is_a?(Array) + end } join_model.name = "HABTM_#{association_name.to_s.camelize}" join_model.table_name_resolver = habtm - join_model.class_resolver = lhs_model + join_model.left_model = lhs_model join_model.add_left_association :left_side, anonymous_class: lhs_model join_model.add_right_association association_name, belongs_to_options(options) @@ -85,7 +94,7 @@ module ActiveRecord::Associations::Builder def middle_reflection(join_model) middle_name = [lhs_model.name.downcase.pluralize, - association_name].join('_'.freeze).gsub('::'.freeze, '_'.freeze).to_sym + association_name].join("_".freeze).gsub("::".freeze, "_".freeze).to_sym middle_options = middle_options join_model HasMany.create_reflection(lhs_model, @@ -96,29 +105,29 @@ module ActiveRecord::Associations::Builder private - def middle_options(join_model) - middle_options = {} - middle_options[:class_name] = "#{lhs_model.name}::#{join_model.name}" - middle_options[:source] = join_model.left_reflection.name - if options.key? :foreign_key - middle_options[:foreign_key] = options[:foreign_key] + def middle_options(join_model) + middle_options = {} + middle_options[:class_name] = "#{lhs_model.name}::#{join_model.name}" + middle_options[:source] = join_model.left_reflection.name + if options.key? :foreign_key + middle_options[:foreign_key] = options[:foreign_key] + end + middle_options end - middle_options - end - def belongs_to_options(options) - rhs_options = {} + def belongs_to_options(options) + rhs_options = {} - if options.key? :class_name - rhs_options[:foreign_key] = options[:class_name].to_s.foreign_key - rhs_options[:class_name] = options[:class_name] - end + if options.key? :class_name + rhs_options[:foreign_key] = options[:class_name].to_s.foreign_key + rhs_options[:class_name] = options[:class_name] + end - if options.key? :association_foreign_key - rhs_options[:foreign_key] = options[:association_foreign_key] - end + if options.key? :association_foreign_key + rhs_options[:foreign_key] = options[:association_foreign_key] + end - rhs_options - end + rhs_options + end end end diff --git a/activerecord/lib/active_record/associations/builder/has_many.rb b/activerecord/lib/active_record/associations/builder/has_many.rb index 1c1b47bd56..7864d4c536 100644 --- a/activerecord/lib/active_record/associations/builder/has_many.rb +++ b/activerecord/lib/active_record/associations/builder/has_many.rb @@ -1,11 +1,11 @@ -module ActiveRecord::Associations::Builder +module ActiveRecord::Associations::Builder # :nodoc: class HasMany < CollectionAssociation #:nodoc: def self.macro :has_many end def self.valid_options(options) - super + [:primary_key, :dependent, :as, :through, :source, :source_type, :inverse_of, :counter_cache, :join_table, :foreign_type] + super + [:primary_key, :dependent, :as, :through, :source, :source_type, :inverse_of, :counter_cache, :join_table, :foreign_type, :index_errors] end def self.valid_dependent_options diff --git a/activerecord/lib/active_record/associations/builder/has_one.rb b/activerecord/lib/active_record/associations/builder/has_one.rb index a272d3c781..4de846d12b 100644 --- a/activerecord/lib/active_record/associations/builder/has_one.rb +++ b/activerecord/lib/active_record/associations/builder/has_one.rb @@ -1,11 +1,11 @@ -module ActiveRecord::Associations::Builder +module ActiveRecord::Associations::Builder # :nodoc: class HasOne < SingularAssociation #:nodoc: def self.macro :has_one end def self.valid_options(options) - valid = super + [:as, :foreign_type] + valid = super + [:as] valid += [:through, :source, :source_type] if options[:through] valid end diff --git a/activerecord/lib/active_record/associations/builder/singular_association.rb b/activerecord/lib/active_record/associations/builder/singular_association.rb index 42542f188e..7732b63af6 100644 --- a/activerecord/lib/active_record/associations/builder/singular_association.rb +++ b/activerecord/lib/active_record/associations/builder/singular_association.rb @@ -1,14 +1,23 @@ # This class is inherited by the has_one and belongs_to association classes -module ActiveRecord::Associations::Builder +module ActiveRecord::Associations::Builder # :nodoc: class SingularAssociation < Association #:nodoc: def self.valid_options(options) - super + [:dependent, :primary_key, :inverse_of, :required] + super + [:foreign_type, :dependent, :primary_key, :inverse_of, :required] end def self.define_accessors(model, reflection) super - define_constructors(model.generated_association_methods, reflection.name) if reflection.constructable? + mixin = model.generated_association_methods + name = reflection.name + + define_constructors(mixin, name) if reflection.constructable? + + mixin.class_eval <<-CODE, __FILE__, __LINE__ + 1 + def reload_#{name} + association(:#{name}).force_reload_reader + end + CODE end # Defines the (build|create)_association methods for belongs_to or has_one association diff --git a/activerecord/lib/active_record/associations/collection_association.rb b/activerecord/lib/active_record/associations/collection_association.rb index 6caadb4ce8..974405e019 100644 --- a/activerecord/lib/active_record/associations/collection_association.rb +++ b/activerecord/lib/active_record/associations/collection_association.rb @@ -10,9 +10,9 @@ module ActiveRecord # HasManyAssociation => has_many # HasManyThroughAssociation + ThroughAssociation => has_many :through # - # CollectionAssociation class provides common methods to the collections + # The CollectionAssociation class provides common methods to the collections # defined by +has_and_belongs_to_many+, +has_many+ or +has_many+ with - # +:through association+ option. + # the +:through association+ option. # # You need to be careful with assumptions regarding the target: The proxy # does not fetch records from the database until it needs them, but new @@ -24,12 +24,9 @@ module ActiveRecord # If you need to work on all current children, new and existing records, # +load_target+ and the +loaded+ flag are your friends. class CollectionAssociation < Association #:nodoc: - # Implements the reader method, e.g. foo.items for Foo.has_many :items - def reader(force_reload = false) - if force_reload - klass.uncached { reload } - elsif stale_target? + def reader + if stale_target? reload end @@ -54,17 +51,26 @@ module ActiveRecord record.send(reflection.association_primary_key) end else - column = "#{reflection.quoted_table_name}.#{reflection.association_primary_key}" - scope.pluck(column) + @association_ids ||= ( + column = "#{reflection.quoted_table_name}.#{reflection.association_primary_key}" + scope.pluck(column) + ) end end # Implements the ids writer method, e.g. foo.item_ids= for Foo.has_many :items def ids_writer(ids) - pk_type = reflection.primary_key_type + pk_type = reflection.association_primary_key_type ids = Array(ids).reject(&:blank?) ids.map! { |i| pk_type.cast(i) } - replace(klass.find(ids).index_by(&:id).values_at(*ids)) + records = klass.where(reflection.association_primary_key => ids).index_by do |r| + r.send(reflection.association_primary_key) + end.values_at(*ids).compact + if records.size != ids.size + klass.all.raise_record_not_found_exception!(ids, records.size, ids.size, reflection.association_primary_key) + else + replace(records) + end end def reset @@ -72,14 +78,6 @@ module ActiveRecord @target = [] end - def select(*fields) - if block_given? - load_target.select.each { |e| yield e } - else - scope.select(*fields) - end - end - def find(*args) if block_given? load_target.find(*args) { |*block_args| yield(*block_args) } @@ -101,44 +99,6 @@ module ActiveRecord end end - def first(*args) - first_nth_or_last(:first, *args) - end - - def second(*args) - first_nth_or_last(:second, *args) - end - - def third(*args) - first_nth_or_last(:third, *args) - end - - def fourth(*args) - first_nth_or_last(:fourth, *args) - end - - def fifth(*args) - first_nth_or_last(:fifth, *args) - end - - def forty_two(*args) - first_nth_or_last(:forty_two, *args) - end - - def last(*args) - first_nth_or_last(:last, *args) - end - - def take(n = nil) - if loaded? - n ? target.take(n) : target.first - else - scope.take(n).tap do |record| - set_inverse_instance record if record.is_a? ActiveRecord::Base - end - end - end - def build(attributes = {}, &block) if attributes.is_a?(Array) attributes.collect { |attr| build(attr, &block) } @@ -149,14 +109,6 @@ module ActiveRecord end end - def create(attributes = {}, &block) - _create_record(attributes, &block) - end - - def create!(attributes = {}, &block) - _create_record(attributes, true, &block) - end - # Add +records+ to this association. Returns +self+ so method calls may # be chained. Since << flattens its argument list and inserts each record, # +push+ and +concat+ behave identically. @@ -204,12 +156,12 @@ module ActiveRecord end dependent = if dependent - dependent - elsif options[:dependent] == :destroy - :delete_all - else - options[:dependent] - end + dependent + elsif options[:dependent] == :destroy + :delete_all + else + options[:dependent] + end delete_or_nullify_all_records(dependent).tap do reset @@ -227,28 +179,6 @@ module ActiveRecord end end - # Count all records using SQL. Construct options and pass them with - # scope to the target class's +count+. - def count(column_name = nil) - relation = scope - if association_scope.distinct_value - # This is needed because 'SELECT count(DISTINCT *)..' is not valid SQL. - column_name ||= reflection.klass.primary_key - relation = relation.distinct - end - - value = relation.count(column_name) - - limit = options[:limit] - offset = options[:offset] - - if limit || offset - [ [value - offset.to_i, 0].max, limit.to_i ].min - else - value - end - end - # Removes +records+ from this association calling +before_remove+ and # +after_remove+ callbacks. # @@ -258,11 +188,8 @@ module ActiveRecord # +delete_records+. They are in any case removed from the collection. def delete(*records) return if records.empty? - _options = records.extract_options! - dependent = _options[:dependent] || options[:dependent] - - records = find(records) if records.any? { |record| record.kind_of?(Fixnum) || record.kind_of?(String) } - delete_or_destroy(records, dependent) + records = find(records) if records.any? { |record| record.kind_of?(Integer) || record.kind_of?(String) } + delete_or_destroy(records, options[:dependent]) end # Deletes the +records+ and removes them from this association calling @@ -272,7 +199,7 @@ module ActiveRecord # +:dependent+ option. def destroy(*records) return if records.empty? - records = find(records) if records.any? { |record| record.kind_of?(Fixnum) || record.kind_of?(String) } + records = find(records) if records.any? { |record| record.kind_of?(Integer) || record.kind_of?(String) } delete_or_destroy(records, :destroy) end @@ -288,14 +215,10 @@ module ActiveRecord # +count_records+, which is a method descendants have to provide. def size if !find_target? || loaded? - if association_scope.distinct_value - target.uniq.size - else - target.size - end - elsif !loaded? && !association_scope.group_values.empty? + target.size + elsif !association_scope.group_values.empty? load_target.size - elsif !loaded? && !association_scope.distinct_value && target.is_a?(Array) + elsif !association_scope.distinct_value && target.is_a?(Array) unsaved_records = target.select(&:new_record?) unsaved_records.size + count_records else @@ -303,15 +226,6 @@ module ActiveRecord end end - # Returns the size of the collection calling +size+ on the target. - # - # If the collection has been already loaded +length+ and +size+ are - # equivalent. If not and you are going to need the records anyway this - # method will take one less query. Otherwise +size+ is more efficient. - def length - load_target.size - end - # Returns true if the collection is empty. # # If the collection has been loaded @@ -328,36 +242,6 @@ module ActiveRecord end end - # Returns true if the collections is not empty. - # If block given, loads all records and checks for one or more matches. - # Otherwise, equivalent to +!collection.empty?+. - def any? - if block_given? - load_target.any? { |*block_args| yield(*block_args) } - else - !empty? - end - end - - # Returns true if the collection has more than 1 record. - # If block given, loads all records and checks for two or more matches. - # Otherwise, equivalent to +collection.size > 1+. - def many? - if block_given? - load_target.many? { |*block_args| yield(*block_args) } - else - size > 1 - end - end - - def distinct - seen = {} - load_target.find_all do |record| - seen[record.id] = true unless seen.key?(record.id) - end - end - alias uniq distinct - # Replace this collection with +other_array+. This will perform a diff # and delete/add only records that have changed. def replace(other_array) @@ -406,12 +290,24 @@ module ActiveRecord def replace_on_target(record, index, skip_callbacks) callback(:before_add, record) unless skip_callbacks - yield(record) if block_given? - if index - @target[index] = record - else - @target << record + begin + if index + record_was = target[index] + target[index] = record + else + target << record + end + + yield(record) if block_given? + rescue + if index + target[index] = record_was + else + target.delete(record) + end + + raise end callback(:after_add, record) unless skip_callbacks @@ -430,31 +326,29 @@ module ActiveRecord owner.new_record? && !foreign_key_present? end - private - def get_records - if reflection.scope_chain.any?(&:any?) || - scope.eager_loading? || - klass.scope_attributes? - - return scope.to_a - end - - conn = klass.connection - sc = reflection.association_scope_cache(conn, owner) do - StatementCache.create(conn) { |params| - as = AssociationScope.create { params.bind } - target_scope.merge as.scope(self, conn) - } - end - - binds = AssociationScope.get_bind_values(owner, reflection.chain) - sc.execute binds, klass, klass.connection + def find_from_target? + loaded? || + owner.new_record? || + target.any? { |record| record.new_record? || record.changed? } end + private + def find_target - records = get_records - records.each { |record| set_inverse_instance(record) } - records + return scope.to_a if skip_statement_cache? + + conn = klass.connection + sc = reflection.association_scope_cache(conn, owner) do + StatementCache.create(conn) { |params| + as = AssociationScope.create { params.bind } + target_scope.merge as.scope(self, conn) + } + end + + binds = AssociationScope.get_bind_values(owner, reflection.chain) + sc.execute(binds, klass, conn) do |record| + set_inverse_instance(record) + end end # We have some records loaded from the database (persisted) and some that are @@ -474,7 +368,7 @@ module ActiveRecord persisted.map! do |record| if mem_record = memory.delete(record) - ((record.attribute_names & mem_record.attribute_names) - mem_record.changes.keys).each do |name| + ((record.attribute_names & mem_record.attribute_names) - mem_record.changed_attribute_names_to_save).each do |name| mem_record[name] = record[name] end @@ -534,8 +428,9 @@ module ActiveRecord records.each { |record| callback(:after_remove, record) } end - # Delete the given records from the association, using one of the methods :destroy, - # :delete_all or :nullify (or nil, in which case a default is used). + # Delete the given records from the association, + # using one of the methods +:destroy+, +:delete_all+ + # or +:nullify+ (or +nil+, in which case a default is used). def delete_records(records, method) raise NotImplementedError end @@ -584,25 +479,6 @@ module ActiveRecord owner.class.send(full_callback_name) end - # Should we deal with assoc.first or assoc.last by issuing an independent query to - # the database, or by getting the target, and then taking the first/last item from that? - # - # If the args is just a non-empty options hash, go to the database. - # - # Otherwise, go to the database only if none of the following are true: - # * target already loaded - # * owner is new record - # * target contains new or changed record(s) - def fetch_first_nth_or_last_using_find?(args) - if args.first.is_a?(Hash) - true - else - !(loaded? || - owner.new_record? || - target.any? { |record| record.new_record? || record.changed? }) - end - end - def include_in_memory?(record) if reflection.is_a?(ActiveRecord::Reflection::ThroughReflection) assoc = owner.association(reflection.through_reflection.name) @@ -629,16 +505,6 @@ module ActiveRecord load_target.select { |r| ids.include?(r.id.to_s) } end end - - # Fetches the first/last using SQL if possible, otherwise from the target array. - def first_nth_or_last(type, *args) - args.shift if args.first.is_a?(Hash) && args.first.empty? - - collection = fetch_first_nth_or_last_using_find?(args) ? scope : load_target - collection.send(type, *args).tap do |record| - set_inverse_instance record if record.is_a? ActiveRecord::Base - end - 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 ddeafb40ea..0d84805b4d 100644 --- a/activerecord/lib/active_record/associations/collection_proxy.rb +++ b/activerecord/lib/active_record/associations/collection_proxy.rb @@ -28,8 +28,7 @@ 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) - delegate :find_nth, to: :scope + delegate :exists?, :update_all, :arel, to: :scope def initialize(klass, association) #:nodoc: @association = association @@ -54,6 +53,12 @@ module ActiveRecord @association.loaded? end + ## + # :method: select + # + # :call-seq: + # select(*fields, &block) + # # Works in two ways. # # *First:* Specify a subset of fields to be selected from the result set. @@ -101,18 +106,9 @@ module ActiveRecord # # #<Pet id: 2, name: "Spook", person_id: 1>, # # #<Pet id: 3, name: "Choo-Choo", person_id: 1> # # ] - # - # person.pets.select(:name) { |pet| pet.name =~ /oo/ } - # # => [ - # # #<Pet id: 2, name: "Spook">, - # # #<Pet id: 3, name: "Choo-Choo"> - # # ] - def select(*fields, &block) - @association.select(*fields, &block) - end # Finds an object in the collection responding to the +id+. Uses the same - # rules as <tt>ActiveRecord::Base.find</tt>. Returns <tt>ActiveRecord::RecordNotFound</tt> + # rules as ActiveRecord::Base.find. Returns ActiveRecord::RecordNotFound # error if the object cannot be found. # # class Person < ActiveRecord::Base @@ -127,7 +123,7 @@ module ActiveRecord # # ] # # person.pets.find(1) # => #<Pet id: 1, name: "Fancy-Fancy", person_id: 1> - # person.pets.find(4) # => ActiveRecord::RecordNotFound: Couldn't find Pet with id=4 + # person.pets.find(4) # => ActiveRecord::RecordNotFound: Couldn't find Pet with 'id'=4 # # person.pets.find(2) { |pet| pet.name.downcase! } # # => #<Pet id: 2, name: "fancy-fancy", person_id: 1> @@ -141,6 +137,12 @@ module ActiveRecord @association.find(*args, &block) end + ## + # :method: first + # + # :call-seq: + # first(limit = nil) + # # 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. @@ -167,35 +169,63 @@ module ActiveRecord # another_person_without.pets # => [] # another_person_without.pets.first # => nil # another_person_without.pets.first(3) # => [] - def first(*args) - @association.first(*args) - end - # Same as +first+ except returns only the second record. - def second(*args) - @association.second(*args) - end + ## + # :method: second + # + # :call-seq: + # second() + # + # Same as #first except returns only the second record. - # Same as +first+ except returns only the third record. - def third(*args) - @association.third(*args) - end + ## + # :method: third + # + # :call-seq: + # third() + # + # Same as #first except returns only the third record. - # Same as +first+ except returns only the fourth record. - def fourth(*args) - @association.fourth(*args) - end + ## + # :method: fourth + # + # :call-seq: + # fourth() + # + # Same as #first except returns only the fourth record. - # Same as +first+ except returns only the fifth record. - def fifth(*args) - @association.fifth(*args) - end + ## + # :method: fifth + # + # :call-seq: + # fifth() + # + # Same as #first except returns only the fifth record. - # Same as +first+ except returns only the forty second record. + ## + # :method: forty_two + # + # :call-seq: + # forty_two() + # + # Same as #first except returns only the forty second record. # Also known as accessing "the reddit". - def forty_two(*args) - @association.forty_two(*args) - end + + ## + # :method: third_to_last + # + # :call-seq: + # third_to_last() + # + # Same as #first except returns only the third-to-last record. + + ## + # :method: second_to_last + # + # :call-seq: + # second_to_last() + # + # Same as #first except returns only the second-to-last record. # 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 @@ -223,12 +253,39 @@ module ActiveRecord # another_person_without.pets # => [] # another_person_without.pets.last # => nil # another_person_without.pets.last(3) # => [] - def last(*args) - @association.last(*args) + def last(limit = nil) + load_target if find_from_target? + super end - def take(n = nil) - @association.take(n) + # Gives a record (or N records if a parameter is supplied) from the collection + # using the same rules as <tt>ActiveRecord::Base.take</tt>. + # + # class Person < ActiveRecord::Base + # has_many :pets + # end + # + # person.pets + # # => [ + # # #<Pet id: 1, name: "Fancy-Fancy", person_id: 1>, + # # #<Pet id: 2, name: "Spook", person_id: 1>, + # # #<Pet id: 3, name: "Choo-Choo", person_id: 1> + # # ] + # + # person.pets.take # => #<Pet id: 1, name: "Fancy-Fancy", person_id: 1> + # + # person.pets.take(2) + # # => [ + # # #<Pet id: 1, name: "Fancy-Fancy", person_id: 1>, + # # #<Pet id: 2, name: "Spook", person_id: 1> + # # ] + # + # another_person_without.pets # => [] + # another_person_without.pets.take # => nil + # another_person_without.pets.take(2) # => [] + def take(limit = nil) + load_target if find_from_target? + super end # Returns a new object of the collection type that has been instantiated @@ -290,7 +347,7 @@ module ActiveRecord @association.create(attributes, &block) end - # Like +create+, except that if the record is invalid, raises an exception. + # Like #create, except that if the record is invalid, raises an exception. # # class Person # has_many :pets @@ -307,8 +364,8 @@ module ActiveRecord end # Add one or more records to the collection by setting their foreign keys - # to the association's primary key. Since << flattens its argument list and - # inserts each record, +push+ and +concat+ behave identically. Returns +self+ + # to the association's primary key. Since #<< flattens its argument list and + # inserts each record, +push+ and #concat behave identically. Returns +self+ # so method calls may be chained. # # class Person < ActiveRecord::Base @@ -364,7 +421,7 @@ module ActiveRecord # specified by the +:dependent+ option. If no +:dependent+ option is given, # then it will follow the default strategy. # - # For +has_many :through+ associations, the default deletion strategy is + # For <tt>has_many :through</tt> associations, the default deletion strategy is # +:delete_all+. # # For +has_many+ associations, the default deletion strategy is +:nullify+. @@ -399,7 +456,7 @@ module ActiveRecord # # #<Pet id: 3, name: "Choo-Choo", person_id: nil> # # ] # - # Both +has_many+ and +has_many :through+ dependencies default to the + # Both +has_many+ and <tt>has_many :through</tt> dependencies default to the # +:delete_all+ strategy if the +:dependent+ option is set to +:destroy+. # Records are not instantiated and callbacks will not be fired. # @@ -418,7 +475,7 @@ module ActiveRecord # person.pets.delete_all # # Pet.find(1, 2, 3) - # # => ActiveRecord::RecordNotFound + # # => ActiveRecord::RecordNotFound: Couldn't find all Pets with 'id': (1, 2, 3) # # If it is set to <tt>:delete_all</tt>, all the objects are deleted # *without* calling their +destroy+ method. @@ -438,7 +495,7 @@ module ActiveRecord # person.pets.delete_all # # Pet.find(1, 2, 3) - # # => ActiveRecord::RecordNotFound + # # => ActiveRecord::RecordNotFound: Couldn't find all Pets with 'id': (1, 2, 3) def delete_all(dependent = nil) @association.delete_all(dependent) end @@ -475,7 +532,7 @@ module ActiveRecord # then it will follow the default strategy. Returns an array with the # deleted records. # - # For +has_many :through+ associations, the default deletion strategy is + # For <tt>has_many :through</tt> associations, the default deletion strategy is # +:delete_all+. # # For +has_many+ associations, the default deletion strategy is +:nullify+. @@ -532,7 +589,7 @@ module ActiveRecord # # => [#<Pet id: 2, name: "Spook", person_id: 1>] # # Pet.find(1, 3) - # # => ActiveRecord::RecordNotFound: Couldn't find all Pets with IDs (1, 3) + # # => ActiveRecord::RecordNotFound: Couldn't find all Pets with 'id': (1, 3) # # If it is set to <tt>:delete_all</tt>, all the +records+ are deleted # *without* calling their +destroy+ method. @@ -560,9 +617,9 @@ module ActiveRecord # # ] # # Pet.find(1) - # # => ActiveRecord::RecordNotFound: Couldn't find Pet with id=1 + # # => ActiveRecord::RecordNotFound: Couldn't find Pet with 'id'=1 # - # You can pass +Fixnum+ or +String+ values, it finds the records + # You can pass +Integer+ or +String+ values, it finds the records # responding to the +id+ and executes delete on them. # # class Person < ActiveRecord::Base @@ -624,9 +681,9 @@ module ActiveRecord # person.pets.size # => 0 # person.pets # => [] # - # Pet.find(1, 2, 3) # => ActiveRecord::RecordNotFound: Couldn't find all Pets with IDs (1, 2, 3) + # Pet.find(1, 2, 3) # => ActiveRecord::RecordNotFound: Couldn't find all Pets with 'id': (1, 2, 3) # - # You can pass +Fixnum+ or +String+ values, it finds the records + # You can pass +Integer+ or +String+ values, it finds the records # responding to the +id+ and then deletes them from the database. # # person.pets.size # => 3 @@ -656,11 +713,17 @@ module ActiveRecord # person.pets.size # => 0 # person.pets # => [] # - # Pet.find(4, 5, 6) # => ActiveRecord::RecordNotFound: Couldn't find all Pets with IDs (4, 5, 6) + # Pet.find(4, 5, 6) # => ActiveRecord::RecordNotFound: Couldn't find all Pets with 'id': (4, 5, 6) def destroy(*records) @association.destroy(*records) end + ## + # :method: distinct + # + # :call-seq: + # distinct(value = true) + # # Specifies whether the records should be unique or not. # # class Person < ActiveRecord::Base @@ -675,17 +738,39 @@ module ActiveRecord # # person.pets.select(:name).distinct # # => [#<Pet name: "Fancy-Fancy">] - def distinct - @association.distinct + # + # person.pets.select(:name).distinct.distinct(false) + # # => [ + # # #<Pet name: "Fancy-Fancy">, + # # #<Pet name: "Fancy-Fancy"> + # # ] + + #-- + def uniq + load_target.uniq end - alias uniq distinct - # Count all records using SQL. + def calculate(operation, column_name) + null_scope? ? scope.calculate(operation, column_name) : super + end + + def pluck(*column_names) + null_scope? ? scope.pluck(*column_names) : super + end + + ## + # :method: count + # + # :call-seq: + # count(column_name = nil, &block) + # + # Count all records. # # class Person < ActiveRecord::Base # has_many :pets # end # + # # This will perform the count using SQL. # person.pets.count # => 3 # person.pets # # => [ @@ -693,9 +778,11 @@ module ActiveRecord # # #<Pet id: 2, name: "Spook", person_id: 1>, # # #<Pet id: 3, name: "Choo-Choo", person_id: 1> # # ] - def count(column_name = nil) - @association.count(column_name) - end + # + # Passing a block will select all of a person's pets in SQL and then + # perform the count using Ruby. + # + # person.pets.count { |pet| pet.name.include?('-') } # => 2 # Returns the size of the collection. If the collection hasn't been loaded, # it executes a <tt>SELECT COUNT(*)</tt> query. Else it calls <tt>collection.size</tt>. @@ -725,6 +812,12 @@ module ActiveRecord @association.size end + ## + # :method: length + # + # :call-seq: + # length() + # # Returns the size of the collection calling +size+ on the target. # If the collection has been already loaded, +length+ and +size+ are # equivalent. If not and you are going to need the records anyway this @@ -745,14 +838,11 @@ module ActiveRecord # # #<Pet id: 2, name: "Spook", person_id: 1>, # # #<Pet id: 3, name: "Choo-Choo", person_id: 1> # # ] - def length - @association.length - end # Returns +true+ if the collection is empty. If the collection has been # loaded it is equivalent # to <tt>collection.size.zero?</tt>. If the collection has not been loaded, - # it is equivalent to <tt>collection.exists?</tt>. If the collection has + # it is equivalent to <tt>!collection.exists?</tt>. If the collection has # not already been loaded and you are going to fetch the records anyway it # is better to check <tt>collection.length.zero?</tt>. # @@ -771,6 +861,12 @@ module ActiveRecord @association.empty? end + ## + # :method: any? + # + # :call-seq: + # any?() + # # Returns +true+ if the collection is not empty. # # class Person < ActiveRecord::Base @@ -781,7 +877,7 @@ module ActiveRecord # person.pets.any? # => false # # person.pets << Pet.new(name: 'Snoop') - # person.pets.count # => 0 + # person.pets.count # => 1 # person.pets.any? # => true # # You can also pass a +block+ to define criteria. The behavior @@ -800,10 +896,13 @@ module ActiveRecord # pet.group == 'dogs' # end # # => true - def any?(&block) - @association.any?(&block) - end + ## + # :method: many? + # + # :call-seq: + # many?() + # # Returns true if the collection has more than one record. # Equivalent to <tt>collection.size > 1</tt>. # @@ -838,9 +937,6 @@ module ActiveRecord # pet.group == 'cats' # end # # => true - def many?(&block) - @association.many?(&block) - end # Returns +true+ if the given +record+ is present in the collection. # @@ -856,10 +952,6 @@ module ActiveRecord !!@association.include?(record) end - def arel - scope.arel - end - def proxy_association @association end @@ -944,6 +1036,10 @@ module ActiveRecord end alias_method :to_a, :to_ary + def records # :nodoc: + load_target + end + # Adds one or more +records+ to the collection by setting their foreign keys # to the association's primary key. Returns +self+, so several appends may be # chained together. @@ -971,7 +1067,7 @@ module ActiveRecord alias_method :append, :<< def prepend(*args) - raise NoMethodError, "prepend on association is not defined. Please use << or append" + raise NoMethodError, "prepend on association is not defined. Please use <<, push or append" end # Equivalent to +delete_all+. The difference is that returns +self+, instead @@ -1029,6 +1125,30 @@ module ActiveRecord proxy_association.reset_scope self end + + private + + def find_nth_with_limit(index, limit) + load_target if find_from_target? + super + end + + def find_nth_from_last(index) + load_target if find_from_target? + super + end + + def null_scope? + @association.null_scope? + end + + def find_from_target? + @association.find_from_target? + end + + def exec_queries + load_target + end end end end diff --git a/activerecord/lib/active_record/associations/foreign_association.rb b/activerecord/lib/active_record/associations/foreign_association.rb index fe48ecec29..3ceec0ee46 100644 --- a/activerecord/lib/active_record/associations/foreign_association.rb +++ b/activerecord/lib/active_record/associations/foreign_association.rb @@ -1,5 +1,5 @@ module ActiveRecord::Associations - module ForeignAssociation + module ForeignAssociation # :nodoc: def foreign_key_present? if reflection.klass.primary_key owner.attribute_present?(reflection.active_record_primary_key) diff --git a/activerecord/lib/active_record/associations/has_many_association.rb b/activerecord/lib/active_record/associations/has_many_association.rb index ca27c9fdde..c5a7d92a2b 100644 --- a/activerecord/lib/active_record/associations/has_many_association.rb +++ b/activerecord/lib/active_record/associations/has_many_association.rb @@ -15,19 +15,17 @@ module ActiveRecord when :restrict_with_error unless empty? - record = klass.human_attribute_name(reflection.name).downcase - owner.errors.add(:base, :"restrict_dependent_destroy.many", record: record) + record = owner.class.human_attribute_name(reflection.name).downcase + owner.errors.add(:base, :'restrict_dependent_destroy.has_many', record: record) throw(:abort) end + when :destroy + # No point in executing the counter update since we're going to destroy the parent anyway + load_target.each { |t| t.destroyed_by_association = reflection } + destroy_all else - if options[:dependent] == :destroy - # No point in executing the counter update since we're going to destroy the parent anyway - load_target.each { |t| t.destroyed_by_association = reflection } - destroy_all - else - delete_all - end + delete_all end end @@ -36,14 +34,14 @@ module ActiveRecord set_inverse_instance(record) if raise - record.save!(:validate => validate) + record.save!(validate: validate) else - record.save(:validate => validate) + record.save(validate: validate) end end def empty? - if has_cached_counter? + if reflection.has_cached_counter? size.zero? else super @@ -66,8 +64,8 @@ module ActiveRecord # If the collection is empty the target is set to an empty array and # the loaded flag is set to true as well. def count_records - count = if has_cached_counter? - owner._read_attribute cached_counter_attribute_name + count = if reflection.has_cached_counter? + owner._read_attribute(reflection.counter_cache_column).to_i else scope.count end @@ -75,75 +73,25 @@ module ActiveRecord # If there's nothing in the database and @target has no new records # we are certain the current target is an empty array. This is a # documented side-effect of the method that may avoid an extra SELECT. - @target ||= [] and loaded! if count == 0 + (@target ||= []) && loaded! if count == 0 [association_scope.limit_value, count].compact.min end - def has_cached_counter?(reflection = reflection()) - owner.attribute_present?(cached_counter_attribute_name(reflection)) - end - - def cached_counter_attribute_name(reflection = reflection()) - if reflection.options[:counter_cache] - reflection.options[:counter_cache].to_s - else - "#{reflection.name}_count" - end - end - def update_counter(difference, reflection = reflection()) - update_counter_in_database(difference, reflection) - update_counter_in_memory(difference, reflection) - end - - def update_counter_in_database(difference, reflection = reflection()) - if has_cached_counter?(reflection) - counter = cached_counter_attribute_name(reflection) - owner.class.update_counters(owner.id, counter => difference) + if reflection.has_cached_counter? + owner.increment!(reflection.counter_cache_column, difference) end end def update_counter_in_memory(difference, reflection = reflection()) - if counter_must_be_updated_by_has_many?(reflection) - counter = cached_counter_attribute_name(reflection) - owner[counter] += difference - owner.send(:clear_attribute_changes, counter) # eww + if reflection.counter_must_be_updated_by_has_many? + counter = reflection.counter_cache_column + owner.increment(counter, difference) + owner.send(:clear_attribute_change, counter) # eww end end - # This shit is nasty. We need to avoid the following situation: - # - # * An associated record is deleted via record.destroy - # * Hence the callbacks run, and they find a belongs_to on the record with a - # :counter_cache options which points back at our owner. So they update the - # counter cache. - # * In which case, we must make sure to *not* update the counter cache, or else - # it will be decremented twice. - # - # Hence this method. - def inverse_which_updates_counter_cache(reflection = reflection()) - counter_name = cached_counter_attribute_name(reflection) - inverse_which_updates_counter_named(counter_name, reflection) - end - alias inverse_updates_counter_cache? inverse_which_updates_counter_cache - - def inverse_which_updates_counter_named(counter_name, reflection) - reflection.klass._reflections.values.find { |inverse_reflection| - inverse_reflection.belongs_to? && - inverse_reflection.counter_cache_column == counter_name - } - end - - def inverse_updates_counter_in_memory?(reflection) - inverse = inverse_which_updates_counter_cache(reflection) - inverse && inverse == reflection.inverse_of - end - - def counter_must_be_updated_by_has_many?(reflection) - !inverse_updates_counter_in_memory?(reflection) && has_cached_counter?(reflection) - end - def delete_count(method, scope) if method == :delete_all scope.delete_all @@ -161,7 +109,7 @@ module ActiveRecord def delete_records(records, method) if method == :destroy records.each(&:destroy!) - update_counter(-records.length) unless inverse_updates_counter_cache? + update_counter(-records.length) unless reflection.inverse_updates_counter_cache? else scope = self.scope.where(reflection.klass.primary_key => records) update_counter(-delete_count(method, scope)) diff --git a/activerecord/lib/active_record/associations/has_many_through_association.rb b/activerecord/lib/active_record/associations/has_many_through_association.rb index cd79266952..c4a7fe4432 100644 --- a/activerecord/lib/active_record/associations/has_many_through_association.rb +++ b/activerecord/lib/active_record/associations/has_many_through_association.rb @@ -38,10 +38,12 @@ module ActiveRecord def insert_record(record, validate = true, raise = false) ensure_not_nested - if raise - record.save!(:validate => validate) - else - return unless record.save(:validate => validate) + if record.new_record? || record.has_changes_to_save? + if raise + record.save!(validate: validate) + else + return unless record.save(validate: validate) + end end save_through_record(record) @@ -66,6 +68,11 @@ module ActiveRecord through_record = through_association.build(*options_for_through_record) through_record.send("#{source_reflection.name}=", record) + + if options[:source_type] + through_record.send("#{source_reflection.foreign_type}=", options[:source_type]) + end + through_record end end @@ -81,7 +88,10 @@ module ActiveRecord end def save_through_record(record) - build_through_record(record).save! + association = build_through_record(record) + if association.changed? + association.save! + end ensure @through_records.delete(record.object_id) end @@ -110,7 +120,7 @@ module ActiveRecord def update_through_counter?(method) case method when :destroy - !inverse_updates_counter_cache?(through_reflection) + !through_reflection.inverse_updates_counter_cache? when :nullify false else @@ -133,7 +143,7 @@ module ActiveRecord if scope.klass.primary_key count = scope.destroy_all.length else - scope.each { |record| record.run_callbacks :destroy } + scope.each(&:_run_destroy_callbacks) arel = scope.arel @@ -141,7 +151,7 @@ module ActiveRecord stmt.from scope.klass.arel_table stmt.wheres = arel.constraints - count = scope.klass.connection.delete(stmt, 'SQL', scope.bound_attributes) + count = scope.klass.connection.delete(stmt, "SQL", scope.bound_attributes) end when :nullify count = scope.update_all(source_reflection.foreign_key => nil) @@ -191,7 +201,7 @@ module ActiveRecord def find_target return [] unless target_reflection_has_associated_record? - get_records + super end # NOTE - not sure that we can actually cope with inverses here diff --git a/activerecord/lib/active_record/associations/has_one_association.rb b/activerecord/lib/active_record/associations/has_one_association.rb index 41a75b820e..b624154def 100644 --- a/activerecord/lib/active_record/associations/has_one_association.rb +++ b/activerecord/lib/active_record/associations/has_one_association.rb @@ -1,5 +1,5 @@ module ActiveRecord - # = Active Record Belongs To Has One Association + # = Active Record Has One Association module Associations class HasOneAssociation < SingularAssociation #:nodoc: include ForeignAssociation @@ -11,8 +11,8 @@ module ActiveRecord when :restrict_with_error if load_target - record = klass.human_attribute_name(reflection.name).downcase - owner.errors.add(:base, :"restrict_dependent_destroy.one", record: record) + record = owner.class.human_attribute_name(reflection.name).downcase + owner.errors.add(:base, :'restrict_dependent_destroy.has_one', record: record) throw(:abort) end @@ -25,10 +25,10 @@ module ActiveRecord raise_on_type_mismatch!(record) if record load_target - return self.target if !(target || record) + return target unless target || record assigning_another_record = target != record - if assigning_another_record || record.changed? + if assigning_another_record || record.has_changes_to_save? save &&= owner.persisted? transaction_if(save) do @@ -53,12 +53,12 @@ module ActiveRecord def delete(method = options[:dependent]) if load_target case method - when :delete - target.delete - when :destroy - target.destroy - when :nullify - target.update_columns(reflection.foreign_key => nil) + when :delete + target.delete + when :destroy + target.destroy + when :nullify + target.update_columns(reflection.foreign_key => nil) if target.persisted? end end end @@ -75,18 +75,19 @@ module ActiveRecord def remove_target!(method) case method - when :delete - target.delete - when :destroy - target.destroy - else - nullify_owner_attributes(target) - - if target.persisted? && owner.persisted? && !target.save - set_owner_attributes(target) - raise RecordNotSaved, "Failed to remove the existing associated #{reflection.name}. " + - "The record failed to save after its foreign key was set to nil." - end + when :delete + target.delete + when :destroy + target.destroy + else + nullify_owner_attributes(target) + remove_inverse_instance(target) + + if target.persisted? && owner.persisted? && !target.save + set_owner_attributes(target) + raise RecordNotSaved, "Failed to remove the existing associated #{reflection.name}. " + + "The record failed to save after its foreign key was set to nil." + end end end diff --git a/activerecord/lib/active_record/associations/has_one_through_association.rb b/activerecord/lib/active_record/associations/has_one_through_association.rb index 08e0ec691f..604904abcc 100644 --- a/activerecord/lib/active_record/associations/has_one_through_association.rb +++ b/activerecord/lib/active_record/associations/has_one_through_association.rb @@ -15,7 +15,7 @@ module ActiveRecord ensure_not_nested through_proxy = owner.association(through_reflection.name) - through_record = through_proxy.send(:load_target) + through_record = through_proxy.load_target if through_record && !record through_record.destroy diff --git a/activerecord/lib/active_record/associations/join_dependency.rb b/activerecord/lib/active_record/associations/join_dependency.rb index 81eb5136a1..4cd1e64c3d 100644 --- a/activerecord/lib/active_record/associations/join_dependency.rb +++ b/activerecord/lib/active_record/associations/join_dependency.rb @@ -1,18 +1,18 @@ module ActiveRecord module Associations class JoinDependency # :nodoc: - autoload :JoinBase, 'active_record/associations/join_dependency/join_base' - autoload :JoinAssociation, 'active_record/associations/join_dependency/join_association' + autoload :JoinBase, "active_record/associations/join_dependency/join_base" + autoload :JoinAssociation, "active_record/associations/join_dependency/join_association" class Aliases # :nodoc: def initialize(tables) @tables = tables - @alias_cache = tables.each_with_object({}) { |table,h| - h[table.node] = table.columns.each_with_object({}) { |column,i| + @alias_cache = tables.each_with_object({}) { |table, h| + h[table.node] = table.columns.each_with_object({}) { |column, i| i[column.name] = column.alias } } - @name_and_alias_cache = tables.each_with_object({}) { |table,h| + @name_and_alias_cache = tables.each_with_object({}) { |table, h| h[table.node] = table.columns.map { |column| [column.name, column.alias] } @@ -32,7 +32,7 @@ module ActiveRecord @alias_cache[node][column] end - class Table < Struct.new(:node, :columns) + class Table < Struct.new(:node, :columns) # :nodoc: def table Arel::Nodes::TableAlias.new node.table, node.aliased_table_name end @@ -62,7 +62,7 @@ module ActiveRecord walk_tree assoc, hash end when Hash - associations.each do |k,v| + associations.each do |k, v| cache = hash[k] ||= {} walk_tree v, cache end @@ -92,8 +92,9 @@ module ActiveRecord # associations # => [:appointments] # joins # => [] # - def initialize(base, associations, joins) + def initialize(base, associations, joins, eager_loading: true) @alias_tracker = AliasTracker.create_with_joins(base.connection, base.table_name, joins, base.type_caster) + @eager_loading = eager_loading tree = self.class.make_tree associations @join_root = JoinBase.new base, build(tree, base) @join_root.children.each { |child| construct_tables! @join_root, child } @@ -103,9 +104,14 @@ module ActiveRecord join_root.drop(1).map!(&:reflection) end - def join_constraints(outer_joins) + def join_constraints(outer_joins, join_type) joins = join_root.children.flat_map { |child| - make_inner_joins join_root, child + + if join_type == Arel::Nodes::OuterJoin + make_left_outer_joins join_root, child + else + make_inner_joins join_root, child + end } joins.concat outer_joins.flat_map { |oj| @@ -120,8 +126,8 @@ module ActiveRecord end def aliases - Aliases.new join_root.each_with_index.map { |join_part,i| - columns = join_part.column_names.each_with_index.map { |column_name,j| + Aliases.new join_root.each_with_index.map { |join_part, i| + columns = join_part.column_names.each_with_index.map { |column_name, j| Aliases::Column.new column_name, "t#{i}_r#{j}" } Aliases::Table.new(join_part, columns) @@ -131,13 +137,13 @@ module ActiveRecord def instantiate(result_set, aliases) primary_key = aliases.column_alias(join_root, join_root.primary_key) - seen = Hash.new { |h,parent_klass| - h[parent_klass] = Hash.new { |i,parent_id| - i[parent_id] = Hash.new { |j,child_klass| j[child_klass] = {} } + seen = Hash.new { |i, object_id| + i[object_id] = Hash.new { |j, child_class| + j[child_class] = {} } } - model_cache = Hash.new { |h,klass| h[klass] = {} } + model_cache = Hash.new { |h, klass| h[klass] = {} } parents = model_cache[join_root] column_aliases = aliases.column_aliases join_root @@ -148,9 +154,10 @@ module ActiveRecord class_name: join_root.base_klass.name } - message_bus.instrument('instantiation.active_record', payload) do + message_bus.instrument("instantiation.active_record", payload) do result_set.each { |row_hash| - parent = parents[row_hash[primary_key]] ||= join_root.instantiate(row_hash, column_aliases) + parent_key = primary_key ? row_hash[primary_key] : row_hash + parent = parents[parent_key] ||= join_root.instantiate(row_hash, column_aliases) construct(parent, join_root, row_hash, result_set, seen, model_cache, aliases) } end @@ -160,126 +167,140 @@ module ActiveRecord private - def make_constraints(parent, child, tables, join_type) - chain = child.reflection.chain - foreign_table = parent.table - foreign_klass = parent.base_klass - child.join_constraints(foreign_table, foreign_klass, child, join_type, tables, child.reflection.scope_chain, chain) - end - - def make_outer_joins(parent, child) - tables = table_aliases_for(parent, child) - join_type = Arel::Nodes::OuterJoin - info = make_constraints parent, child, tables, join_type + def make_constraints(parent, child, tables, join_type) + chain = child.reflection.chain + foreign_table = parent.table + foreign_klass = parent.base_klass + child.join_constraints(foreign_table, foreign_klass, child, join_type, tables, child.reflection.scope_chain, chain) + end - [info] + child.children.flat_map { |c| make_outer_joins(child, c) } - end + def make_outer_joins(parent, child) + tables = table_aliases_for(parent, child) + join_type = Arel::Nodes::OuterJoin + info = make_constraints parent, child, tables, join_type - def make_inner_joins(parent, child) - tables = child.tables - join_type = Arel::Nodes::InnerJoin - info = make_constraints parent, child, tables, join_type + [info] + child.children.flat_map { |c| make_outer_joins(child, c) } + end - [info] + child.children.flat_map { |c| make_inner_joins(child, c) } - end + def make_left_outer_joins(parent, child) + tables = child.tables + join_type = Arel::Nodes::OuterJoin + info = make_constraints parent, child, tables, join_type - def table_aliases_for(parent, node) - node.reflection.chain.map { |reflection| - alias_tracker.aliased_table_for( - reflection.table_name, - table_alias_for(reflection, parent, reflection != node.reflection) - ) - } - end + [info] + child.children.flat_map { |c| make_left_outer_joins(child, c) } + end - def construct_tables!(parent, node) - node.tables = table_aliases_for(parent, node) - node.children.each { |child| construct_tables! node, child } - end + def make_inner_joins(parent, child) + tables = child.tables + join_type = Arel::Nodes::InnerJoin + info = make_constraints parent, child, tables, join_type - def table_alias_for(reflection, parent, join) - name = "#{reflection.plural_name}_#{parent.table_name}" - name << "_join" if join - name - end + [info] + child.children.flat_map { |c| make_inner_joins(child, c) } + end - def walk(left, right) - intersection, missing = right.children.map { |node1| - [left.children.find { |node2| node1.match? node2 }, node1] - }.partition(&:first) + def table_aliases_for(parent, node) + node.reflection.chain.map { |reflection| + alias_tracker.aliased_table_for( + reflection.table_name, + table_alias_for(reflection, parent, reflection != node.reflection) + ) + } + end - ojs = missing.flat_map { |_,n| make_outer_joins left, n } - intersection.flat_map { |l,r| walk l, r }.concat ojs - end + def construct_tables!(parent, node) + node.tables = table_aliases_for(parent, node) + node.children.each { |child| construct_tables! node, child } + end - def find_reflection(klass, name) - klass._reflect_on_association(name) or - raise ConfigurationError, "Association named '#{ name }' was not found on #{ klass.name }; perhaps you misspelled it?" - end + def table_alias_for(reflection, parent, join) + name = "#{reflection.plural_name}_#{parent.table_name}" + name << "_join" if join + name + end - def build(associations, base_klass) - associations.map do |name, right| - reflection = find_reflection base_klass, name - reflection.check_validity! - reflection.check_eager_loadable! + def walk(left, right) + intersection, missing = right.children.map { |node1| + [left.children.find { |node2| node1.match? node2 }, node1] + }.partition(&:first) - if reflection.polymorphic? - raise EagerLoadPolymorphicError.new(reflection) - end + ojs = missing.flat_map { |_, n| make_outer_joins left, n } + intersection.flat_map { |l, r| walk l, r }.concat ojs + end - JoinAssociation.new reflection, build(right, reflection.klass) + def find_reflection(klass, name) + klass._reflect_on_association(name) || + raise(ConfigurationError, "Can't join '#{klass.name}' to association named '#{name}'; perhaps you misspelled it?") end - end - def construct(ar_parent, parent, row, rs, seen, model_cache, aliases) - return if ar_parent.nil? - primary_id = ar_parent.id + def build(associations, base_klass) + associations.map do |name, right| + reflection = find_reflection base_klass, name + reflection.check_validity! + reflection.check_eager_loadable! - parent.children.each do |node| - if node.reflection.collection? - other = ar_parent.association(node.reflection.name) - other.loaded! - elsif ar_parent.association_cached?(node.reflection.name) - model = ar_parent.association(node.reflection.name).target - construct(model, node, row, rs, seen, model_cache, aliases) - next - end + if reflection.polymorphic? + next unless @eager_loading + raise EagerLoadPolymorphicError.new(reflection) + end + + JoinAssociation.new reflection, build(right, reflection.klass) + end.compact + end - key = aliases.column_alias(node, node.primary_key) - id = row[key] - if id.nil? - nil_association = ar_parent.association(node.reflection.name) - nil_association.loaded! - next + def construct(ar_parent, parent, row, rs, seen, model_cache, aliases) + return if ar_parent.nil? + + parent.children.each do |node| + if node.reflection.collection? + other = ar_parent.association(node.reflection.name) + other.loaded! + elsif ar_parent.association_cached?(node.reflection.name) + model = ar_parent.association(node.reflection.name).target + construct(model, node, row, rs, seen, model_cache, aliases) + next + end + + key = aliases.column_alias(node, node.primary_key) + id = row[key] + if id.nil? + nil_association = ar_parent.association(node.reflection.name) + nil_association.loaded! + next + end + + model = seen[ar_parent.object_id][node.base_klass][id] + + if model + construct(model, node, row, rs, seen, model_cache, aliases) + else + model = construct_model(ar_parent, node, row, model_cache, id, aliases) + + if node.reflection.scope_for(node.base_klass).readonly_value + model.readonly! + end + + seen[ar_parent.object_id][node.base_klass][id] = model + construct(model, node, row, rs, seen, model_cache, aliases) + end end + end + + def construct_model(record, node, row, model_cache, id, aliases) + other = record.association(node.reflection.name) - model = seen[parent.base_klass][primary_id][node.base_klass][id] + model = model_cache[node][id] ||= + node.instantiate(row, aliases.column_aliases(node)) do |m| + other.set_inverse_instance(m) + end - if model - construct(model, node, row, rs, seen, model_cache, aliases) + if node.reflection.collection? + other.target.push(model) else - model = construct_model(ar_parent, node, row, model_cache, id, aliases) - model.readonly! - seen[parent.base_klass][primary_id][node.base_klass][id] = model - construct(model, node, row, rs, seen, model_cache, aliases) + other.target = model end - end - end - def construct_model(record, node, row, model_cache, id, aliases) - model = model_cache[node][id] ||= node.instantiate(row, - aliases.column_aliases(node)) - other = record.association(node.reflection.name) - - if node.reflection.collection? - other.target.push(model) - else - other.target = model + model end - - other.set_inverse_instance(model) - model - end end end end diff --git a/activerecord/lib/active_record/associations/join_dependency/join_association.rb b/activerecord/lib/active_record/associations/join_dependency/join_association.rb index a6ad09a38a..a5705951f3 100644 --- a/activerecord/lib/active_record/associations/join_dependency/join_association.rb +++ b/activerecord/lib/active_record/associations/join_dependency/join_association.rb @@ -1,4 +1,4 @@ -require 'active_record/associations/join_dependency/join_part' +require "active_record/associations/join_dependency/join_part" module ActiveRecord module Associations @@ -54,12 +54,20 @@ module ActiveRecord end scope_chain_index += 1 - relation = ActiveRecord::Relation.create( - klass, - table, - predicate_builder, - ) - scope_chain_items.concat [klass.send(:build_default_scope, relation)].compact + klass_scope = + if klass.current_scope + klass.current_scope.clone.tap { |scope| + scope.joins_values = [] + } + else + relation = ActiveRecord::Relation.create( + klass, + table, + predicate_builder, + ) + klass.send(:build_default_scope, relation) + end + scope_chain_items.concat [klass_scope].compact rel = scope_chain_items.inject(scope_chain_items.shift) do |left, right| left.merge right @@ -74,9 +82,8 @@ module ActiveRecord value = foreign_klass.base_class.name column = klass.columns_hash[reflection.type.to_s] - substitute = klass.connection.substitute_at(column) binds << Relation::QueryAttribute.new(column.name, value, klass.type_for_attribute(column.name)) - constraint = constraint.and table[reflection.type].eq substitute + constraint = constraint.and klass.arel_attribute(reflection.type, table).eq(Arel::Nodes::BindParam.new) end joins << table.create_join(table, table.create_on(constraint), join_type) diff --git a/activerecord/lib/active_record/associations/join_dependency/join_base.rb b/activerecord/lib/active_record/associations/join_dependency/join_base.rb index 3a26c25737..fca20514d1 100644 --- a/activerecord/lib/active_record/associations/join_dependency/join_base.rb +++ b/activerecord/lib/active_record/associations/join_dependency/join_base.rb @@ -1,4 +1,4 @@ -require 'active_record/associations/join_dependency/join_part' +require "active_record/associations/join_dependency/join_part" module ActiveRecord module Associations diff --git a/activerecord/lib/active_record/associations/join_dependency/join_part.rb b/activerecord/lib/active_record/associations/join_dependency/join_part.rb index 9c6573f913..61cec5403a 100644 --- a/activerecord/lib/active_record/associations/join_dependency/join_part.rb +++ b/activerecord/lib/active_record/associations/join_dependency/join_part.rb @@ -15,7 +15,7 @@ module ActiveRecord # association. attr_reader :base_klass, :children - delegate :table_name, :column_names, :primary_key, :to => :base_klass + delegate :table_name, :column_names, :primary_key, to: :base_klass def initialize(base_klass, children) @base_klass = base_klass @@ -62,8 +62,8 @@ module ActiveRecord hash end - def instantiate(row, aliases) - base_klass.instantiate(extract_record(row, aliases)) + def instantiate(row, aliases, &block) + base_klass.instantiate(extract_record(row, aliases), &block) end end end diff --git a/activerecord/lib/active_record/associations/preloader.rb b/activerecord/lib/active_record/associations/preloader.rb index 6ecc741195..9f77f38b35 100644 --- a/activerecord/lib/active_record/associations/preloader.rb +++ b/activerecord/lib/active_record/associations/preloader.rb @@ -42,18 +42,20 @@ module ActiveRecord extend ActiveSupport::Autoload 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 :BelongsTo, 'active_record/associations/preloader/belongs_to' + 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 :BelongsTo, "active_record/associations/preloader/belongs_to" end + NULL_RELATION = Struct.new(:values, :where_clause, :joins_values).new({}, Relation::WhereClause.empty, []) + # Eager loads the named associations for the given Active Record record(s). # # In this description, 'association name' shall refer to the name passed @@ -88,9 +90,6 @@ module ActiveRecord # [ :books, :author ] # { author: :avatar } # [ :books, { author: :avatar } ] - - NULL_RELATION = Struct.new(:values, :where_clause, :joins_values).new({}, Relation::WhereClause.empty, []) - def preload(records, associations, preload_scope = nil) records = Array.wrap(records).compact.uniq associations = Array.wrap(associations) @@ -107,97 +106,108 @@ module ActiveRecord private - def preloaders_on(association, records, scope) - case association - when Hash - preloaders_for_hash(association, records, scope) - when Symbol - preloaders_for_one(association, records, scope) - when String - preloaders_for_one(association.to_sym, records, scope) - else - raise ArgumentError, "#{association.inspect} was not recognised for preload" + # Loads all the given data into +records+ for the +association+. + def preloaders_on(association, records, scope) + case association + when Hash + preloaders_for_hash(association, records, scope) + when Symbol + preloaders_for_one(association, records, scope) + when String + preloaders_for_one(association.to_sym, records, scope) + else + raise ArgumentError, "#{association.inspect} was not recognized for preload" + end end - end - def preloaders_for_hash(association, records, scope) - association.flat_map { |parent, child| - loaders = preloaders_for_one parent, records, scope + def preloaders_for_hash(association, records, scope) + association.flat_map { |parent, child| + loaders = preloaders_for_one parent, records, scope - recs = loaders.flat_map(&:preloaded_records).uniq - loaders.concat Array.wrap(child).flat_map { |assoc| - preloaders_on assoc, recs, scope + recs = loaders.flat_map(&:preloaded_records).uniq + loaders.concat Array.wrap(child).flat_map { |assoc| + preloaders_on assoc, recs, scope + } + loaders } - loaders - } - end + end - # Not all records have the same class, so group then preload group on the reflection - # itself so that if various subclass share the same association then we do not split - # them unnecessarily - # - # Additionally, polymorphic belongs_to associations can have multiple associated - # classes, depending on the polymorphic_type field. So we group by the classes as - # well. - def preloaders_for_one(association, records, scope) - grouped_records(association, records).flat_map do |reflection, klasses| - klasses.map do |rhs_klass, rs| - loader = preloader_for(reflection, rs, rhs_klass).new(rhs_klass, rs, reflection, scope) - loader.run self - loader + # Loads all the given data into +records+ for a singular +association+. + # + # Functions by instantiating a preloader class such as Preloader::HasManyThrough and + # call the +run+ method for each passed in class in the +records+ argument. + # + # Not all records have the same class, so group then preload group on the reflection + # itself so that if various subclass share the same association then we do not split + # them unnecessarily + # + # Additionally, polymorphic belongs_to associations can have multiple associated + # classes, depending on the polymorphic_type field. So we group by the classes as + # well. + def preloaders_for_one(association, records, scope) + grouped_records(association, records).flat_map do |reflection, klasses| + klasses.map do |rhs_klass, rs| + loader = preloader_for(reflection, rs, rhs_klass).new(rhs_klass, rs, reflection, scope) + loader.run self + loader + end end end - end - def grouped_records(association, records) - h = {} - records.each do |record| - next unless record - assoc = record.association(association) - klasses = h[assoc.reflection] ||= {} - (klasses[assoc.klass] ||= []) << record + def grouped_records(association, records) + h = {} + records.each do |record| + next unless record + assoc = record.association(association) + klasses = h[assoc.reflection] ||= {} + (klasses[assoc.klass] ||= []) << record + end + h end - h - end - class AlreadyLoaded # :nodoc: - attr_reader :owners, :reflection + class AlreadyLoaded # :nodoc: + attr_reader :owners, :reflection - def initialize(klass, owners, reflection, preload_scope) - @owners = owners - @reflection = reflection - end + def initialize(klass, owners, reflection, preload_scope) + @owners = owners + @reflection = reflection + end - def run(preloader); end + def run(preloader); end - def preloaded_records - owners.flat_map { |owner| owner.association(reflection.name).target } + def preloaded_records + owners.flat_map { |owner| owner.association(reflection.name).target } + end end - end - class NullPreloader # :nodoc: - def self.new(klass, owners, reflection, preload_scope); self; end - def self.run(preloader); end - def self.preloaded_records; []; end - end + class NullPreloader # :nodoc: + def self.new(klass, owners, reflection, preload_scope); self; end + def self.run(preloader); end + def self.preloaded_records; []; end + def self.owners; []; end + end - def preloader_for(reflection, owners, rhs_klass) - return NullPreloader unless rhs_klass + # Returns a class containing the logic needed to load preload the data + # and attach it to a relation. For example +Preloader::Association+ or + # +Preloader::HasManyThrough+. The class returned implements a `run` method + # that accepts a preloader. + def preloader_for(reflection, owners, rhs_klass) + return NullPreloader unless rhs_klass - if owners.first.association(reflection.name).loaded? - return AlreadyLoaded - end - reflection.check_preloadable! - - case reflection.macro - when :has_many - reflection.options[:through] ? HasManyThrough : HasMany - when :has_one - reflection.options[:through] ? HasOneThrough : HasOne - when :belongs_to - BelongsTo + if owners.first.association(reflection.name).loaded? + return AlreadyLoaded + end + reflection.check_preloadable! + + case reflection.macro + when :has_many + reflection.options[:through] ? HasManyThrough : HasMany + when :has_one + reflection.options[:through] ? HasOneThrough : HasOne + when :belongs_to + BelongsTo + end end - end end end end diff --git a/activerecord/lib/active_record/associations/preloader/association.rb b/activerecord/lib/active_record/associations/preloader/association.rb index 1dc8bff193..4072d19380 100644 --- a/activerecord/lib/active_record/associations/preloader/association.rb +++ b/activerecord/lib/active_record/associations/preloader/association.rb @@ -12,7 +12,6 @@ module ActiveRecord @preload_scope = preload_scope @model = owners.first && owners.first.class @scope = nil - @owners_by_key = nil @preloaded_records = [] end @@ -29,10 +28,6 @@ module ActiveRecord end def records_for(ids) - query_scope(ids) - end - - def query_scope(ids) scope.where(association_key_name => ids) end @@ -48,7 +43,7 @@ module ActiveRecord # This is overridden by HABTM as the condition should be on the foreign_key column in # the join table def association_key - table[association_key_name] + klass.arel_attribute(association_key_name, table) end # The name of the key on the model which declares the association @@ -56,107 +51,119 @@ module ActiveRecord raise NotImplementedError end - def owners_by_key - @owners_by_key ||= if key_conversion_required? - owners.group_by do |owner| - owner[owner_key_name].to_s - end - else - owners.group_by do |owner| - owner[owner_key_name] - end - end - end - def options reflection.options end private - def associated_records_by_owner(preloader) - owners_map = owners_by_key - owner_keys = owners_map.keys.compact + def associated_records_by_owner(preloader) + records = load_records do |record| + owner = owners_by_key[convert_key(record[association_key_name])] + association = owner.association(reflection.name) + association.set_inverse_instance(record) + end - # Each record may have multiple owners, and vice-versa - records_by_owner = owners.each_with_object({}) do |owner,h| - h[owner] = [] + owners.each_with_object({}) do |owner, result| + result[owner] = records[convert_key(owner[owner_key_name])] || [] + end end - if owner_keys.any? - # Some databases impose a limit on the number of ids in a list (in Oracle it's 1000) - # Make several smaller queries if necessary or make one query if the adapter supports it - sliced = owner_keys.each_slice(klass.connection.in_clause_length || owner_keys.size) + def owner_keys + unless defined?(@owner_keys) + @owner_keys = owners.map do |owner| + owner[owner_key_name] + end + @owner_keys.uniq! + @owner_keys.compact! + end + @owner_keys + end - records = load_slices sliced - records.each do |record, owner_key| - owners_map[owner_key].each do |owner| - records_by_owner[owner] << record + def owners_by_key + unless defined?(@owners_by_key) + @owners_by_key = owners.each_with_object({}) do |owner, h| + h[convert_key(owner[owner_key_name])] = owner end end + @owners_by_key end - records_by_owner - end + def key_conversion_required? + @key_conversion_required ||= association_key_type != owner_key_type + end - def key_conversion_required? - association_key_type != owner_key_type - end + def convert_key(key) + if key_conversion_required? + key.to_s + else + key + end + end - def association_key_type - @klass.type_for_attribute(association_key_name.to_s).type - end + def association_key_type + @klass.type_for_attribute(association_key_name.to_s).type + end - def owner_key_type - @model.type_for_attribute(owner_key_name.to_s).type - end + def owner_key_type + @model.type_for_attribute(owner_key_name.to_s).type + end - def load_slices(slices) - @preloaded_records = slices.flat_map { |slice| - records_for(slice) - } + def load_records(&block) + return {} if owner_keys.empty? + # Some databases impose a limit on the number of ids in a list (in Oracle it's 1000) + # Make several smaller queries if necessary or make one query if the adapter supports it + slices = owner_keys.each_slice(klass.connection.in_clause_length || owner_keys.size) + @preloaded_records = slices.flat_map do |slice| + records_for(slice).load(&block) + end + @preloaded_records.group_by do |record| + convert_key(record[association_key_name]) + end + end - @preloaded_records.map { |record| - key = record[association_key_name] - key = key.to_s if key_conversion_required? + def reflection_scope + @reflection_scope ||= reflection.scope_for(klass) + end - [record, key] - } - end + def build_scope + scope = klass.unscoped - def reflection_scope - @reflection_scope ||= reflection.scope ? klass.unscoped.instance_exec(nil, &reflection.scope) : klass.unscoped - end + values = reflection_scope.values + preload_values = preload_scope.values - def build_scope - scope = klass.unscoped + scope.where_clause = reflection_scope.where_clause + preload_scope.where_clause + scope.references_values = Array(values[:references]) + Array(preload_values[:references]) - values = reflection_scope.values - preload_values = preload_scope.values + if preload_values[:select] || values[:select] + scope._select!(preload_values[:select] || values[:select]) + end + scope.includes! preload_values[:includes] || values[:includes] + if preload_scope.joins_values.any? + scope.joins!(preload_scope.joins_values) + else + scope.joins!(reflection_scope.joins_values) + end - scope.where_clause = reflection_scope.where_clause + preload_scope.where_clause - scope.references_values = Array(values[:references]) + Array(preload_values[:references]) + if order_values = preload_values[:order] || values[:order] + scope.order!(order_values) + end - scope._select! preload_values[:select] || values[:select] || table[Arel.star] - scope.includes! preload_values[:includes] || values[:includes] - if preload_scope.joins_values.any? - scope.joins!(preload_scope.joins_values) - else - scope.joins!(reflection_scope.joins_values) - end - scope.order! preload_values[:order] || values[:order] + if preload_values[:reordering] || values[:reordering] + scope.reordering_value = true + end - if preload_values[:readonly] || values[:readonly] - scope.readonly! - end + if preload_values[:readonly] || values[:readonly] + scope.readonly! + end - if options[:as] - scope.where!(klass.table_name => { reflection.type => model.base_class.sti_name }) - end + if options[:as] + scope.where!(klass.table_name => { reflection.type => model.base_class.sti_name }) + end - scope.unscope_values = Array(values[:unscope]) - klass.default_scoped.merge(scope) - end + scope.unscope_values = Array(values[:unscope]) + Array(preload_values[:unscope]) + klass.default_scoped.merge(scope) + end end end end diff --git a/activerecord/lib/active_record/associations/preloader/belongs_to.rb b/activerecord/lib/active_record/associations/preloader/belongs_to.rb index 5091d4717a..38e231826c 100644 --- a/activerecord/lib/active_record/associations/preloader/belongs_to.rb +++ b/activerecord/lib/active_record/associations/preloader/belongs_to.rb @@ -2,7 +2,6 @@ module ActiveRecord module Associations class Preloader class BelongsTo < SingularAssociation #:nodoc: - def association_key_name reflection.options[:primary_key] || klass && klass.primary_key end @@ -10,7 +9,6 @@ module ActiveRecord def owner_key_name reflection.foreign_key end - end end end diff --git a/activerecord/lib/active_record/associations/preloader/collection_association.rb b/activerecord/lib/active_record/associations/preloader/collection_association.rb index 5adffcd831..26690bf16d 100644 --- a/activerecord/lib/active_record/associations/preloader/collection_association.rb +++ b/activerecord/lib/active_record/associations/preloader/collection_association.rb @@ -2,22 +2,15 @@ module ActiveRecord module Associations class Preloader class CollectionAssociation < Association #:nodoc: - private - def build_scope - super.order(preload_scope.values[:order] || reflection_scope.values[:order]) - end - - def preload(preloader) - associated_records_by_owner(preloader).each do |owner, records| - association = owner.association(reflection.name) - association.loaded! - association.target.concat(records) - records.each { |record| association.set_inverse_instance(record) } + def preload(preloader) + associated_records_by_owner(preloader).each do |owner, records| + association = owner.association(reflection.name) + association.loaded! + association.target.concat(records) + end end - end - end end end diff --git a/activerecord/lib/active_record/associations/preloader/has_many.rb b/activerecord/lib/active_record/associations/preloader/has_many.rb index 3ea91a8c11..20df1cc19a 100644 --- a/activerecord/lib/active_record/associations/preloader/has_many.rb +++ b/activerecord/lib/active_record/associations/preloader/has_many.rb @@ -2,7 +2,6 @@ module ActiveRecord module Associations class Preloader class HasMany < CollectionAssociation #:nodoc: - def association_key_name reflection.foreign_key end @@ -10,7 +9,6 @@ module ActiveRecord def owner_key_name reflection.active_record_primary_key end - end end end diff --git a/activerecord/lib/active_record/associations/preloader/has_one.rb b/activerecord/lib/active_record/associations/preloader/has_one.rb index 24728e9f01..c4add621ca 100644 --- a/activerecord/lib/active_record/associations/preloader/has_one.rb +++ b/activerecord/lib/active_record/associations/preloader/has_one.rb @@ -2,7 +2,6 @@ module ActiveRecord module Associations class Preloader class HasOne < SingularAssociation #:nodoc: - def association_key_name reflection.foreign_key end @@ -10,13 +9,6 @@ module ActiveRecord def owner_key_name reflection.active_record_primary_key end - - private - - def build_scope - super.order(preload_scope.values[:order] || reflection_scope.values[:order]) - end - end end end diff --git a/activerecord/lib/active_record/associations/preloader/singular_association.rb b/activerecord/lib/active_record/associations/preloader/singular_association.rb index f60647a81e..5c5828262e 100644 --- a/activerecord/lib/active_record/associations/preloader/singular_association.rb +++ b/activerecord/lib/active_record/associations/preloader/singular_association.rb @@ -2,19 +2,16 @@ module ActiveRecord module Associations class Preloader class SingularAssociation < Association #:nodoc: - private - def preload(preloader) - associated_records_by_owner(preloader).each do |owner, associated_records| - record = associated_records.first + def preload(preloader) + associated_records_by_owner(preloader).each do |owner, associated_records| + record = associated_records.first - association = owner.association(reflection.name) - association.target = record - association.set_inverse_instance(record) if record + association = owner.association(reflection.name) + association.target = record + end end - end - end end end diff --git a/activerecord/lib/active_record/associations/preloader/through_association.rb b/activerecord/lib/active_record/associations/preloader/through_association.rb index 56aa23b173..9d44a02021 100644 --- a/activerecord/lib/active_record/associations/preloader/through_association.rb +++ b/activerecord/lib/active_record/associations/preloader/through_association.rb @@ -18,12 +18,13 @@ module ActiveRecord through_records = owners.map do |owner| association = owner.association through_reflection.name - [owner, Array(association.reader)] + center = target_records_from_association(association) + [owner, Array(center)] end reset_association owners, through_reflection.name - middle_records = through_records.flat_map { |(_,rec)| rec } + middle_records = through_records.flat_map { |(_, rec)| rec } preloaders = preloader.preload(middle_records, source_reflection.name, @@ -31,64 +32,76 @@ module ActiveRecord @preloaded_records = preloaders.flat_map(&:preloaded_records) - middle_to_pl = preloaders.each_with_object({}) do |pl,h| + middle_to_pl = preloaders.each_with_object({}) do |pl, h| pl.owners.each { |middle| h[middle] = pl } end - record_offset = {} - @preloaded_records.each_with_index do |record,i| - record_offset[record] = i - end - - through_records.each_with_object({}) { |(lhs,center),records_by_owner| + through_records.each_with_object({}) do |(lhs, center), records_by_owner| pl_to_middle = center.group_by { |record| middle_to_pl[record] } records_by_owner[lhs] = pl_to_middle.flat_map do |pl, middles| rhs_records = middles.flat_map { |r| association = r.association source_reflection.name - association.reader + target_records_from_association(association) }.compact - rhs_records.sort_by { |rhs| record_offset[rhs] } + # Respect the order on `reflection_scope` if it exists, else use the natural order. + if reflection_scope.values[:order].present? + @id_map ||= id_to_index_map @preloaded_records + rhs_records.sort_by { |rhs| @id_map[rhs] } + else + rhs_records + end end - } + end end private - def reset_association(owners, association_name) - should_reset = (through_scope != through_reflection.klass.unscoped) || - (reflection.options[:source_type] && through_reflection.collection?) - - # Don't cache the association - we would only be caching a subset - if should_reset - owners.each { |owner| - owner.association(association_name).reset - } + def id_to_index_map(ids) + id_map = {} + ids.each_with_index { |id, index| id_map[id] = index } + id_map end - end + def reset_association(owners, association_name) + should_reset = (through_scope != through_reflection.klass.unscoped) || + (reflection.options[:source_type] && through_reflection.collection?) - def through_scope - scope = through_reflection.klass.unscoped + # Don't cache the association - we would only be caching a subset + if should_reset + owners.each { |owner| + owner.association(association_name).reset + } + end + end - if options[:source_type] - scope.where! reflection.foreign_type => options[:source_type] - else - unless reflection_scope.where_clause.empty? - scope.includes_values = Array(reflection_scope.values[:includes] || options[:source]) - scope.where_clause = reflection_scope.where_clause + def through_scope + scope = through_reflection.klass.unscoped + + if options[:source_type] + scope.where! reflection.foreign_type => options[:source_type] + else + unless reflection_scope.where_clause.empty? + scope.includes_values = Array(reflection_scope.values[:includes] || options[:source]) + scope.where_clause = reflection_scope.where_clause + end + + scope.references! reflection_scope.values[:references] + if scope.eager_loading? && order_values = reflection_scope.values[:order] + scope = scope.order(order_values) + end end - scope.references! reflection_scope.values[:references] - scope = scope.order reflection_scope.values[:order] if scope.eager_loading? + scope end - scope - end + def target_records_from_association(association) + association.loaded? ? association.target : association.reader + end end end end diff --git a/activerecord/lib/active_record/associations/singular_association.rb b/activerecord/lib/active_record/associations/singular_association.rb index bec9505bd2..91580a28d0 100644 --- a/activerecord/lib/active_record/associations/singular_association.rb +++ b/activerecord/lib/active_record/associations/singular_association.rb @@ -2,10 +2,8 @@ module ActiveRecord module Associations class SingularAssociation < Association #:nodoc: # Implements the reader method, e.g. foo.bar for Foo.has_one :bar - def reader(force_reload = false) - if force_reload && klass - klass.uncached { reload } - elsif !loaded? || stale_target? + def reader + if !loaded? || stale_target? reload end @@ -17,14 +15,6 @@ module ActiveRecord replace(record) end - def create(attributes = {}, &block) - _create_record(attributes, &block) - end - - def create!(attributes = {}, &block) - _create_record(attributes, true, &block) - end - def build(attributes = {}) record = build_record(attributes) yield(record) if block_given? @@ -32,19 +22,21 @@ module ActiveRecord record end + # Implements the reload reader method, e.g. foo.reload_bar for + # Foo.has_one :bar + def force_reload_reader + klass.uncached { reload } + target + end + private def create_scope scope.scope_for_create.stringify_keys.except(klass.primary_key) end - def get_records - if reflection.scope_chain.any?(&:any?) || - scope.eager_loading? || - klass.scope_attributes? - - return scope.limit(1).to_a - end + def find_target + return scope.take if skip_statement_cache? conn = klass.connection sc = reflection.association_scope_cache(conn, owner) do @@ -55,13 +47,11 @@ module ActiveRecord end binds = AssociationScope.get_bind_values(owner, reflection.chain) - sc.execute binds, klass, klass.connection - end - - def find_target - if record = get_records.first + sc.execute(binds, klass, conn) do |record| set_inverse_instance record - end + end.first + rescue ::RangeError + nil end def replace(record) diff --git a/activerecord/lib/active_record/associations/through_association.rb b/activerecord/lib/active_record/associations/through_association.rb index 55ee9f04e0..6b87993ba3 100644 --- a/activerecord/lib/active_record/associations/through_association.rb +++ b/activerecord/lib/active_record/associations/through_association.rb @@ -2,10 +2,9 @@ module ActiveRecord # = Active Record Through Association module Associations module ThroughAssociation #:nodoc: + delegate :source_reflection, :through_reflection, to: :reflection - delegate :source_reflection, :through_reflection, :to => :reflection - - protected + private # We merge in these scopes for two reasons: # @@ -22,8 +21,6 @@ module ActiveRecord scope end - private - # Construct attributes for :through pointing to owner and associate. This is used by the # methods which create and delete records on the association. # @@ -76,13 +73,21 @@ module ActiveRecord def ensure_mutable unless source_reflection.belongs_to? - raise HasManyThroughCantAssociateThroughHasOneOrManyReflection.new(owner, reflection) + if reflection.has_one? + raise HasOneThroughCantAssociateThroughHasOneOrManyReflection.new(owner, reflection) + else + raise HasManyThroughCantAssociateThroughHasOneOrManyReflection.new(owner, reflection) + end end end def ensure_not_nested if reflection.nested? - raise HasManyThroughNestedAssociationsAreReadonly.new(owner, reflection) + if reflection.has_one? + raise HasOneThroughNestedAssociationsAreReadonly.new(owner, reflection) + else + raise HasManyThroughNestedAssociationsAreReadonly.new(owner, reflection) + end end end diff --git a/activerecord/lib/active_record/attribute.rb b/activerecord/lib/active_record/attribute.rb index 73dd3fa041..38281158d8 100644 --- a/activerecord/lib/active_record/attribute.rb +++ b/activerecord/lib/active_record/attribute.rb @@ -5,8 +5,8 @@ module ActiveRecord FromDatabase.new(name, value, type) end - def from_user(name, value, type) - FromUser.new(name, value, type) + def from_user(name, value, type, original_attribute = nil) + FromUser.new(name, value, type, original_attribute) end def with_cast_value(name, value, type) @@ -26,36 +26,46 @@ module ActiveRecord # This method should not be called directly. # Use #from_database or #from_user - def initialize(name, value_before_type_cast, type) + def initialize(name, value_before_type_cast, type, original_attribute = nil) @name = name @value_before_type_cast = value_before_type_cast @type = type + @original_attribute = original_attribute end def value # `defined?` is cheaper than `||=` when we get back falsy values - @value = original_value unless defined?(@value) + @value = type_cast(value_before_type_cast) unless defined?(@value) @value end def original_value - type_cast(value_before_type_cast) + if assigned? + original_attribute.original_value + else + type_cast(value_before_type_cast) + end end def value_for_database type.serialize(value) end - def changed_from?(old_value) - type.changed?(old_value, value, value_before_type_cast) + def changed? + changed_from_assignment? || changed_in_place? + end + + def changed_in_place? + has_been_read? && type.changed_in_place?(original_value_for_database, value) end - def changed_in_place_from?(old_value) - has_been_read? && type.changed_in_place?(old_value, value) + def forgetting_assignment + with_value_from_database(value_for_database) end def with_value_from_user(value) - self.class.from_user(name, value, type) + type.assert_valid_value(value) + self.class.from_user(name, value, type, original_attribute || self) end def with_value_from_database(value) @@ -67,7 +77,11 @@ module ActiveRecord end def with_type(type) - self.class.new(name, value_before_type_cast, type) + if changed_in_place? + with_value_from_user(value).with_type(type) + else + self.class.new(name, value_before_type_cast, type, original_attribute) + end end def type_cast(*) @@ -98,77 +112,129 @@ module ActiveRecord [self.class, name, value_before_type_cast, type].hash end - protected - - def initialize_dup(other) - if defined?(@value) && @value.duplicable? - @value = @value.dup - end + def init_with(coder) + @name = coder["name"] + @value_before_type_cast = coder["value_before_type_cast"] + @type = coder["type"] + @original_attribute = coder["original_attribute"] + @value = coder["value"] if coder.map.key?("value") end - class FromDatabase < Attribute # :nodoc: - def type_cast(value) - type.deserialize(value) - end + def encode_with(coder) + coder["name"] = name + coder["value_before_type_cast"] = value_before_type_cast if value_before_type_cast + coder["type"] = type if type + coder["original_attribute"] = original_attribute if original_attribute + coder["value"] = value if defined?(@value) end - class FromUser < Attribute # :nodoc: - def type_cast(value) - type.cast(value) - end + # TODO Change this to private once we've dropped Ruby 2.2 support. + # Workaround for Ruby 2.2 "private attribute?" warning. + protected - def came_from_user? - true - end - end + attr_reader :original_attribute + alias_method :assigned?, :original_attribute - class WithCastValue < Attribute # :nodoc: - def type_cast(value) - value + def original_value_for_database + if assigned? + original_attribute.original_value_for_database + else + _original_value_for_database + end end - def changed_in_place_from?(old_value) - false + private + def initialize_dup(other) + if defined?(@value) && @value.duplicable? + @value = @value.dup + end end - end - class Null < Attribute # :nodoc: - def initialize(name) - super(name, nil, Type::Value.new) + def changed_from_assignment? + assigned? && type.changed?(original_value, value, value_before_type_cast) end - def value - nil + def _original_value_for_database + type.serialize(original_value) end - def with_type(type) - self.class.with_cast_value(name, nil, type) - end + class FromDatabase < Attribute # :nodoc: + def type_cast(value) + type.deserialize(value) + end - def with_value_from_database(value) - raise ActiveModel::MissingAttributeError, "can't write unknown attribute `#{name}`" + def _original_value_for_database + value_before_type_cast + end end - alias_method :with_value_from_user, :with_value_from_database - end - class Uninitialized < Attribute # :nodoc: - def initialize(name, type) - super(name, nil, type) + class FromUser < Attribute # :nodoc: + def type_cast(value) + type.cast(value) + end + + def came_from_user? + true + end end - def value - if block_given? - yield name + class WithCastValue < Attribute # :nodoc: + def type_cast(value) + value + end + + def changed_in_place? + false end end - def value_for_database + class Null < Attribute # :nodoc: + def initialize(name) + super(name, nil, Type.default_value) + end + + def type_cast(*) + nil + end + + def with_type(type) + self.class.with_cast_value(name, nil, type) + end + + def with_value_from_database(value) + raise ActiveModel::MissingAttributeError, "can't write unknown attribute `#{name}`" + end + alias_method :with_value_from_user, :with_value_from_database end - def initialized? - false + class Uninitialized < Attribute # :nodoc: + UNINITIALIZED_ORIGINAL_VALUE = Object.new + + def initialize(name, type) + super(name, nil, type) + end + + def value + if block_given? + yield name + end + end + + def original_value + UNINITIALIZED_ORIGINAL_VALUE + end + + def value_for_database + end + + def initialized? + false + end + + def with_type(type) + self.class.new(name, type) + end end - end - private_constant :FromDatabase, :FromUser, :Null, :Uninitialized, :WithCastValue + private_constant :FromDatabase, :FromUser, :Null, :Uninitialized, :WithCastValue end end diff --git a/activerecord/lib/active_record/attribute/user_provided_default.rb b/activerecord/lib/active_record/attribute/user_provided_default.rb index e0bee8c17e..57f8bbed76 100644 --- a/activerecord/lib/active_record/attribute/user_provided_default.rb +++ b/activerecord/lib/active_record/attribute/user_provided_default.rb @@ -1,32 +1,30 @@ -require 'active_record/attribute' +require "active_record/attribute" module ActiveRecord class Attribute # :nodoc: - class UserProvidedDefault < FromUser + class UserProvidedDefault < FromUser # :nodoc: def initialize(name, value, type, database_default) - super(name, value, type) - @database_default = database_default + @user_provided_value = value + super(name, value, type, database_default) end - def type_cast(value) - if value.is_a?(Proc) - super(value.call) + def value_before_type_cast + if user_provided_value.is_a?(Proc) + @memoized_value_before_type_cast ||= user_provided_value.call else - super + @user_provided_value end end - def changed_in_place_from?(old_value) - super || changed_from?(database_default.value) - end - def with_type(type) - self.class.new(name, value_before_type_cast, type, database_default) + self.class.new(name, user_provided_value, type, original_attribute) end + # TODO Change this to private once we've dropped Ruby 2.2 support. + # Workaround for Ruby 2.2 "private attribute?" warning. protected - attr_reader :database_default + attr_reader :user_provided_value end end end diff --git a/activerecord/lib/active_record/attribute_assignment.rb b/activerecord/lib/active_record/attribute_assignment.rb index 45fdcaa1cd..d0dfca0cac 100644 --- a/activerecord/lib/active_record/attribute_assignment.rb +++ b/activerecord/lib/active_record/attribute_assignment.rb @@ -1,99 +1,91 @@ -require 'active_model/forbidden_attributes_protection' +require "active_model/forbidden_attributes_protection" module ActiveRecord module AttributeAssignment extend ActiveSupport::Concern include ActiveModel::AttributeAssignment - # Alias for `assign_attributes`. See +ActiveModel::AttributeAssignment+. + # Alias for ActiveModel::AttributeAssignment#assign_attributes. See ActiveModel::AttributeAssignment. def attributes=(attributes) assign_attributes(attributes) end private - def _assign_attributes(attributes) # :nodoc: - multi_parameter_attributes = {} - nested_parameter_attributes = {} + def _assign_attributes(attributes) + multi_parameter_attributes = {} + nested_parameter_attributes = {} - attributes.each do |k, v| - if k.include?("(") - multi_parameter_attributes[k] = attributes.delete(k) - elsif v.is_a?(Hash) - nested_parameter_attributes[k] = attributes.delete(k) + attributes.each do |k, v| + if k.include?("(") + multi_parameter_attributes[k] = attributes.delete(k) + elsif v.is_a?(Hash) + nested_parameter_attributes[k] = attributes.delete(k) + end end - end - super(attributes) - - assign_nested_parameter_attributes(nested_parameter_attributes) unless nested_parameter_attributes.empty? - assign_multiparameter_attributes(multi_parameter_attributes) unless multi_parameter_attributes.empty? - end + super(attributes) - # Tries to assign given value to given attribute. - # In case of an error, re-raises with the ActiveRecord constant. - def _assign_attribute(k, v) # :nodoc: - super - rescue ActiveModel::UnknownAttributeError - raise UnknownAttributeError.new(self, k) - end + assign_nested_parameter_attributes(nested_parameter_attributes) unless nested_parameter_attributes.empty? + assign_multiparameter_attributes(multi_parameter_attributes) unless multi_parameter_attributes.empty? + end - # Assign any deferred nested attributes after the base attributes have been set. - def assign_nested_parameter_attributes(pairs) - pairs.each { |k, v| _assign_attribute(k, v) } - end + # Assign any deferred nested attributes after the base attributes have been set. + def assign_nested_parameter_attributes(pairs) + pairs.each { |k, v| _assign_attribute(k, v) } + end - # Instantiates objects for all attribute classes that needs more than one constructor parameter. This is done - # by calling new on the column type or aggregation type (through composed_of) object with these parameters. - # 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 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) - ) - end + # Instantiates objects for all attribute classes that needs more than one constructor parameter. This is done + # by calling new on the column type or aggregation type (through composed_of) object with these parameters. + # So having the pairs written_on(1) = "2004", written_on(2) = "6", written_on(3) = "24", will instantiate + # written_on (a date type) with Date.new("2004", "6", "24"). You can also specify a typecast character in the + # parentheses to have the parameters typecasted before they're used in the constructor. Use i for Integer 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) + ) + end - def execute_callstack_for_multiparameter_attributes(callstack) - errors = [] - callstack.each do |name, values_with_empty_parameters| - begin - if values_with_empty_parameters.each_value.all?(&:nil?) - values = nil - else - values = values_with_empty_parameters + def execute_callstack_for_multiparameter_attributes(callstack) + errors = [] + callstack.each do |name, values_with_empty_parameters| + begin + if values_with_empty_parameters.each_value.all?(&:nil?) + values = nil + else + values = values_with_empty_parameters + end + send("#{name}=", values) + rescue => ex + errors << AttributeAssignmentError.new("error on assignment #{values_with_empty_parameters.values.inspect} to #{name} (#{ex.message})", ex, name) end - send("#{name}=", values) - rescue => ex - errors << AttributeAssignmentError.new("error on assignment #{values_with_empty_parameters.values.inspect} to #{name} (#{ex.message})", ex, name) + end + unless errors.empty? + error_descriptions = errors.map(&:message).join(",") + raise MultiparameterAssignmentErrors.new(errors), "#{errors.size} error(s) on assignment of multiparameter attributes [#{error_descriptions}]" end end - unless errors.empty? - error_descriptions = errors.map(&:message).join(",") - raise MultiparameterAssignmentErrors.new(errors), "#{errors.size} error(s) on assignment of multiparameter attributes [#{error_descriptions}]" - end - end - def extract_callstack_for_multiparameter_attributes(pairs) - attributes = {} + def extract_callstack_for_multiparameter_attributes(pairs) + attributes = {} - pairs.each do |(multiparameter_name, value)| - attribute_name = multiparameter_name.split("(").first - attributes[attribute_name] ||= {} + pairs.each do |(multiparameter_name, value)| + attribute_name = multiparameter_name.split("(").first + attributes[attribute_name] ||= {} - parameter_value = value.empty? ? nil : type_cast_attribute_value(multiparameter_name, value) - attributes[attribute_name][find_parameter_position(multiparameter_name)] ||= parameter_value - end + parameter_value = value.empty? ? nil : type_cast_attribute_value(multiparameter_name, value) + attributes[attribute_name][find_parameter_position(multiparameter_name)] ||= parameter_value + end - attributes - end + attributes + end - def type_cast_attribute_value(multiparameter_name, value) - multiparameter_name =~ /\([0-9]*([if])\)/ ? value.send("to_" + $1) : value - end + def type_cast_attribute_value(multiparameter_name, value) + multiparameter_name =~ /\([0-9]*([if])\)/ ? value.send("to_" + $1) : value + end - def find_parameter_position(multiparameter_name) - multiparameter_name.scan(/\(([0-9]*).*\)/).first.first.to_i - end + def find_parameter_position(multiparameter_name) + multiparameter_name.scan(/\(([0-9]*).*\)/).first.first.to_i + end end end diff --git a/activerecord/lib/active_record/attribute_decorators.rb b/activerecord/lib/active_record/attribute_decorators.rb index 7d0ae32411..c39e9ce4c5 100644 --- a/activerecord/lib/active_record/attribute_decorators.rb +++ b/activerecord/lib/active_record/attribute_decorators.rb @@ -8,12 +8,34 @@ module ActiveRecord end module ClassMethods # :nodoc: + # This method is an internal API used to create class macros such as + # +serialize+, and features like time zone aware attributes. + # + # Used to wrap the type of an attribute in a new type. + # When the schema for a model is loaded, attributes with the same name as + # +column_name+ will have their type yielded to the given block. The + # return value of that block will be used instead. + # + # Subsequent calls where +column_name+ and +decorator_name+ are the same + # will override the previous decorator, not decorate twice. This can be + # used to create idempotent class macros like +serialize+ def decorate_attribute_type(column_name, decorator_name, &block) matcher = ->(name, _) { name == column_name.to_s } key = "_#{column_name}_#{decorator_name}" decorate_matching_attribute_types(matcher, key, &block) end + # This method is an internal API used to create higher level features like + # time zone aware attributes. + # + # When the schema for a model is loaded, +matcher+ will be called for each + # attribute with its name and type. If the matcher returns a truthy value, + # the type will then be yielded to the given block, and the return value + # of that block will replace the type. + # + # Subsequent calls to this method with the same value for +decorator_name+ + # will replace the previous decorator, not decorate twice. This can be + # used to ensure that class macros are idempotent. def decorate_matching_attribute_types(matcher, decorator_name, &block) reload_schema_from_cache decorator_name = decorator_name.to_s @@ -24,13 +46,13 @@ module ActiveRecord private - def load_schema! - super - attribute_types.each do |name, type| - decorated_type = attribute_type_decorations.apply(name, type) - define_attribute(name, decorated_type) + def load_schema! + super + attribute_types.each do |name, type| + decorated_type = attribute_type_decorations.apply(name, type) + define_attribute(name, decorated_type) + end end - end end class TypeDecorator # :nodoc: @@ -53,15 +75,15 @@ module ActiveRecord private - def decorators_for(name, type) - matching(name, type).map(&:last) - end + def decorators_for(name, type) + matching(name, type).map(&:last) + end - def matching(name, type) - @decorations.values.select do |(matcher, _)| - matcher.call(name, type) + def matching(name, type) + @decorations.values.select do |(matcher, _)| + matcher.call(name, type) + end end - end end end end diff --git a/activerecord/lib/active_record/attribute_methods.rb b/activerecord/lib/active_record/attribute_methods.rb index 9d58a19304..1ed1deec55 100644 --- a/activerecord/lib/active_record/attribute_methods.rb +++ b/activerecord/lib/active_record/attribute_methods.rb @@ -1,7 +1,7 @@ -require 'active_support/core_ext/enumerable' -require 'active_support/core_ext/string/filters' -require 'mutex_m' -require 'thread_safe' +require "active_support/core_ext/enumerable" +require "active_support/core_ext/string/filters" +require "mutex_m" +require "concurrent/map" module ActiveRecord # = Active Record Attribute Methods @@ -34,30 +34,6 @@ module ActiveRecord BLACKLISTED_CLASS_METHODS = %w(private public protected allocate new name parent superclass) - class AttributeMethodCache - def initialize - @module = Module.new - @method_cache = ThreadSafe::Cache.new - end - - def [](name) - @method_cache.compute_if_absent(name) do - safe_name = name.unpack('h*').first - temp_method = "__temp__#{safe_name}" - ActiveRecord::AttributeMethods::AttrNames.set_name_cache safe_name, name - @module.module_eval method_body(temp_method, safe_name), __FILE__, __LINE__ - @module.instance_method temp_method - end - end - - private - - # Override this method in the subclasses for method body. - def method_body(method_name, const_name) - raise NotImplementedError, "Subclasses must implement a method_body(method_name, const_name) method." - end - end - class GeneratedAttributeMethods < Module; end # :nodoc: module ClassMethods @@ -96,7 +72,7 @@ module ActiveRecord end end - # Raises a <tt>ActiveRecord::DangerousAttributeError</tt> exception when an + # Raises an ActiveRecord::DangerousAttributeError exception when an # \Active \Record method is defined in the model, otherwise +false+. # # class Person < ActiveRecord::Base @@ -106,7 +82,7 @@ module ActiveRecord # end # # Person.instance_method_already_implemented?(:save) - # # => ActiveRecord::DangerousAttributeError: save is defined by ActiveRecord + # # => ActiveRecord::DangerousAttributeError: save is defined by Active Record. Check to make sure that you don't have an attribute or method with the same name. # # Person.instance_method_already_implemented?(:name) # # => false @@ -172,7 +148,7 @@ module ActiveRecord # Person.attribute_method?(:age=) # => true # Person.attribute_method?(:nothing) # => false def attribute_method?(attribute) - super || (table_exists? && column_names.include?(attribute.to_s.sub(/=$/, ''))) + 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 @@ -185,10 +161,22 @@ module ActiveRecord # # => ["id", "created_at", "updated_at", "name", "age"] def attribute_names @attribute_names ||= if !abstract_class? && table_exists? - attribute_types.keys - else - [] - end + attribute_types.keys + else + [] + end + end + + # Returns true if the given attribute exists, otherwise false. + # + # class Person < ActiveRecord::Base + # end + # + # Person.has_attribute?('name') # => true + # Person.has_attribute?(:age) # => true + # Person.has_attribute?(:nothing) # => false + def has_attribute?(attr_name) + attribute_types.key?(attr_name.to_s) end # Returns the column object for the named attribute. @@ -221,16 +209,24 @@ module ActiveRecord # 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 + # 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) return false unless super - name = name.to_s + + case name + when :to_partial_path + name = "to_partial_path".freeze + when :to_model + name = "to_model".freeze + else + name = name.to_s + end # If the result is true then check for the select case. # For queries selecting a subset of columns, return false for unselected columns. @@ -283,9 +279,8 @@ module ActiveRecord # Returns an <tt>#inspect</tt>-like string for the value of the # attribute +attr_name+. String attributes are truncated up to 50 # characters, Date and Time attributes are returned in the - # <tt>:db</tt> format, Array attributes are truncated up to 10 values. - # Other attributes return the value of <tt>#inspect</tt> without - # modification. + # <tt>:db</tt> format. Other attributes return the value of + # <tt>#inspect</tt> without modification. # # person = Person.create!(name: 'David Heinemeier Hansson ' * 3) # @@ -296,7 +291,7 @@ module ActiveRecord # # => "\"2012-10-22 00:15:07\"" # # person.attribute_for_inspect(:tag_ids) - # # => "[1, 2, 3, 4, 5, 6, 7, 8, 9, 10, ...]" + # # => "[1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11]" def attribute_for_inspect(attr_name) value = read_attribute(attr_name) @@ -304,9 +299,6 @@ module ActiveRecord "#{value[0, 50]}...".inspect elsif value.is_a?(Date) || value.is_a?(Time) %("#{value.to_s(:db)}") - elsif value.is_a?(Array) && value.size > 10 - inspected = value.first(10).inspect - %(#{inspected[0...-1]}, ...]) else value.inspect end @@ -338,8 +330,6 @@ module ActiveRecord # # Note: +:id+ is always present. # - # Alias for the <tt>read_attribute</tt> method. - # # class Person < ActiveRecord::Base # belongs_to :organization # end @@ -356,7 +346,7 @@ module ActiveRecord end # Updates the attribute identified by <tt>attr_name</tt> with the specified +value+. - # (Alias for the protected <tt>write_attribute</tt> method). + # (Alias for the protected #write_attribute method). # # class Person < ActiveRecord::Base # end @@ -364,7 +354,7 @@ module ActiveRecord # person = Person.new # person[:age] = '22' # person[:age] # => 22 - # person[:age] # => Fixnum + # person[:age].class # => Integer def []=(attr_name, value) write_attribute(attr_name, value) end @@ -377,92 +367,92 @@ module ActiveRecord # # For example: # - # class PostsController < ActionController::Base - # after_action :print_accessed_fields, only: :index + # class PostsController < ActionController::Base + # after_action :print_accessed_fields, only: :index # - # def index - # @posts = Post.all - # end + # def index + # @posts = Post.all + # end # - # private + # private # - # def print_accessed_fields - # p @posts.first.accessed_fields + # def print_accessed_fields + # p @posts.first.accessed_fields + # end # end - # end # # Which allows you to quickly change your code to: # - # class PostsController < ActionController::Base - # def index - # @posts = Post.select(:id, :title, :author_id, :updated_at) + # class PostsController < ActionController::Base + # def index + # @posts = Post.select(:id, :title, :author_id, :updated_at) + # end # end - # end def accessed_fields @attributes.accessed end protected - 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 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(attribute_names) # :nodoc: - arel_attributes_with_values(attributes_for_create(attribute_names)) - end + 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) # :nodoc: - arel_attributes_with_values(attributes_for_update(attribute_names)) - end + 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) # :nodoc: - # We check defined? because Syck calls respond_to? before actually calling initialize. - defined?(@attributes) && @attributes.key?(attr_name) - end + def attribute_method?(attr_name) # :nodoc: + # We check defined? because Syck calls respond_to? before actually calling initialize. + defined?(@attributes) && @attributes.key?(attr_name) + end private - # Returns a Hash of the Arel::Attributes and attribute values that have been - # typecasted for use in an Arel insert/update method. - def arel_attributes_with_values(attribute_names) - attrs = {} - arel_table = self.class.arel_table + # Returns a Hash of the Arel::Attributes and attribute values that have been + # typecasted for use in an Arel insert/update method. + def arel_attributes_with_values(attribute_names) + attrs = {} + arel_table = self.class.arel_table - attribute_names.each do |name| - attrs[arel_table[name]] = typecasted_attribute_value(name) + attribute_names.each do |name| + attrs[arel_table[name]] = typecasted_attribute_value(name) + end + attrs end - attrs - end - # Filters the primary keys and readonly attributes from the attribute names. - def attributes_for_update(attribute_names) - attribute_names.reject do |name| - readonly_attribute?(name) + # Filters the primary keys and readonly attributes from the attribute names. + def attributes_for_update(attribute_names) + attribute_names.reject do |name| + readonly_attribute?(name) + end end - end - # Filters out the primary keys, from the attribute names, when the primary - # key is to be generated (e.g. the id attribute has no value). - def attributes_for_create(attribute_names) - attribute_names.reject do |name| - pk_attribute?(name) && id.nil? + # 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(attribute_names) + attribute_names.reject do |name| + pk_attribute?(name) && id.nil? + end end - end - def readonly_attribute?(name) - self.class.readonly_attributes.include?(name) - end + def readonly_attribute?(name) + self.class.readonly_attributes.include?(name) + end - def pk_attribute?(name) - name == self.class.primary_key - end + def pk_attribute?(name) + name == self.class.primary_key + end - def typecasted_attribute_value(name) - _read_attribute(name) - end + def typecasted_attribute_value(name) + _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 56c1898551..115eb1ef3f 100644 --- a/activerecord/lib/active_record/attribute_methods/before_type_cast.rb +++ b/activerecord/lib/active_record/attribute_methods/before_type_cast.rb @@ -2,7 +2,7 @@ module ActiveRecord module AttributeMethods # = Active Record Attribute Methods Before Type Cast # - # <tt>ActiveRecord::AttributeMethods::BeforeTypeCast</tt> provides a way to + # ActiveRecord::AttributeMethods::BeforeTypeCast provides a way to # read the value of the attributes before typecasting and deserialization. # # class Task < ActiveRecord::Base @@ -63,14 +63,14 @@ module ActiveRecord private - # Handle *_before_type_cast for method_missing. - def attribute_before_type_cast(attribute_name) - read_attribute_before_type_cast(attribute_name) - end + # Handle *_before_type_cast for method_missing. + def attribute_before_type_cast(attribute_name) + read_attribute_before_type_cast(attribute_name) + end - def attribute_came_from_user?(attribute_name) - @attributes[attribute_name].came_from_user? - end + def attribute_came_from_user?(attribute_name) + @attributes[attribute_name].came_from_user? + end end end end diff --git a/activerecord/lib/active_record/attribute_methods/dirty.rb b/activerecord/lib/active_record/attribute_methods/dirty.rb index 0171ef3bdf..e20b65e43c 100644 --- a/activerecord/lib/active_record/attribute_methods/dirty.rb +++ b/activerecord/lib/active_record/attribute_methods/dirty.rb @@ -1,4 +1,6 @@ -require 'active_support/core_ext/module/attribute_accessors' +# frozen_string_literal: true +require "active_support/core_ext/module/attribute_accessors" +require "active_record/attribute_mutation_tracker" module ActiveRecord module AttributeMethods @@ -14,6 +16,18 @@ module ActiveRecord class_attribute :partial_writes, instance_writer: false self.partial_writes = true + + after_create { changes_internally_applied } + after_update { changes_internally_applied } + + # Attribute methods for "changed in last call to save?" + attribute_method_affix(prefix: "saved_change_to_", suffix: "?") + attribute_method_prefix("saved_change_to_") + attribute_method_suffix("_before_last_save") + + # Attribute methods for "will change if I call save?" + attribute_method_affix(prefix: "will_save_change_to_", suffix: "?") + attribute_method_suffix("_change_to_be_saved", "_in_database") end # Attempts to +save+ the record and clears changed attributes if successful. @@ -34,23 +48,50 @@ module ActiveRecord # <tt>reload</tt> the record and clears changed attributes. def reload(*) super.tap do - clear_changes_information + @previous_mutation_tracker = nil + clear_mutation_trackers + @changed_attributes = HashWithIndifferentAccess.new end end def initialize_dup(other) # :nodoc: super - calculate_changes_from_defaults + @attributes = self.class._default_attributes.map do |attr| + attr.with_value_from_user(@attributes.fetch_value(attr.name)) + end + clear_mutation_trackers + end + + def changes_internally_applied # :nodoc: + @mutations_before_last_save = mutation_tracker + forget_attribute_assignments + @mutations_from_database = AttributeMutationTracker.new(@attributes) end def changes_applied - super - store_original_raw_attributes + @previous_mutation_tracker = mutation_tracker + @changed_attributes = HashWithIndifferentAccess.new + clear_mutation_trackers end def clear_changes_information + @previous_mutation_tracker = nil + @changed_attributes = HashWithIndifferentAccess.new + forget_attribute_assignments + clear_mutation_trackers + end + + def raw_write_attribute(attr_name, *) + result = super + clear_attribute_change(attr_name) + result + end + + def clear_attribute_changes(attr_names) super - original_raw_attributes.clear + attr_names.each do |attr_name| + clear_attribute_change(attr_name) + end end def changed_attributes @@ -59,130 +100,219 @@ module ActiveRecord if defined?(@cached_changed_attributes) @cached_changed_attributes else - super.reverse_merge(attributes_changed_in_place).freeze + emit_warning_if_needed("changed_attributes", "saved_changes.transform_values(&:first)") + super.reverse_merge(mutation_tracker.changed_values).freeze end end def changes cache_changed_attributes do + emit_warning_if_needed("changes", "saved_changes") super end end + def previous_changes + unless previous_mutation_tracker.equal?(mutations_before_last_save) + ActiveSupport::Deprecation.warn(<<-EOW.strip_heredoc) + The behavior of `previous_changes` inside of after callbacks is + deprecated without replacement. In the next release of Rails, + this method inside of `after_save` will return the changes that + were just saved. + EOW + end + previous_mutation_tracker.changes + end + def attribute_changed_in_place?(attr_name) - old_value = original_raw_attribute(attr_name) - @attributes[attr_name].changed_in_place_from?(old_value) + mutation_tracker.changed_in_place?(attr_name) end - private + # Did this attribute change when we last saved? This method can be invoked + # as `saved_change_to_name?` instead of `saved_change_to_attribute?("name")`. + # Behaves similarly to +attribute_changed?+. This method is useful in + # after callbacks to determine if the call to save changed a certain + # attribute. + # + # ==== Options + # + # +from+ When passed, this method will return false unless the original + # value is equal to the given option + # + # +to+ When passed, this method will return false unless the value was + # changed to the given value + def saved_change_to_attribute?(attr_name, **options) + mutations_before_last_save.changed?(attr_name, **options) + end - def changes_include?(attr_name) - super || attribute_changed_in_place?(attr_name) + # Returns the change to an attribute during the last save. If the + # attribute was changed, the result will be an array containing the + # original value and the saved value. + # + # Behaves similarly to +attribute_change+. This method is useful in after + # callbacks, to see the change in an attribute that just occurred + # + # This method can be invoked as `saved_change_to_name` in instead of + # `saved_change_to_attribute("name")` + def saved_change_to_attribute(attr_name) + mutations_before_last_save.change_to_attribute(attr_name) end - def calculate_changes_from_defaults - @changed_attributes = nil - self.class.column_defaults.each do |attr, orig_value| - set_attribute_was(attr, orig_value) if _field_changed?(attr, orig_value) - end + # Returns the original value of an attribute before the last save. + # Behaves similarly to +attribute_was+. This method is useful in after + # callbacks to get the original value of an attribute before the save that + # just occurred + def attribute_before_last_save(attr_name) + mutations_before_last_save.original_value(attr_name) end - # Wrap write_attribute to remember original attribute value. - def write_attribute(attr, value) - attr = attr.to_s + # Did the last call to `save` have any changes to change? + def saved_changes? + mutations_before_last_save.any_changes? + end - old_value = old_attribute_value(attr) + # Returns a hash containing all the changes that were just saved. + def saved_changes + mutations_before_last_save.changes + end - result = super - store_original_raw_attribute(attr) - save_changed_attribute(attr, old_value) - result + # Alias for `attribute_changed?` + def will_save_change_to_attribute?(attr_name, **options) + mutations_from_database.changed?(attr_name, **options) end - def raw_write_attribute(attr, value) - attr = attr.to_s + # Alias for `attribute_change` + def attribute_change_to_be_saved(attr_name) + mutations_from_database.change_to_attribute(attr_name) + end - result = super - original_raw_attributes[attr] = value - result + # Alias for `attribute_was` + def attribute_in_database(attr_name) + mutations_from_database.original_value(attr_name) end - def save_changed_attribute(attr, old_value) - clear_changed_attributes_cache - if attribute_changed_by_setter?(attr) - clear_attribute_changes(attr) unless _field_changed?(attr, old_value) - else - set_attribute_was(attr, old_value) if _field_changed?(attr, old_value) - end + # Alias for `changed?` + def has_changes_to_save? + mutations_from_database.any_changes? end - def old_attribute_value(attr) - if attribute_changed?(attr) - changed_attributes[attr] - else - clone_attribute_value(:_read_attribute, attr) - end + # Alias for `changes` + def changes_to_save + mutations_from_database.changes end - def _update_record(*) - partial_writes? ? super(keys_for_partial_write) : super + # Alias for `changed` + def changed_attribute_names_to_save + changes_to_save.keys end - def _create_record(*) - partial_writes? ? super(keys_for_partial_write) : super + # Alias for `changed_attributes` + def attributes_in_database + changes_to_save.transform_values(&:first) end - def keys_for_partial_write - changed & self.class.column_names + def attribute_was(*) + emit_warning_if_needed("attribute_was", "attribute_before_last_save") + super end - def _field_changed?(attr, old_value) - @attributes[attr].changed_from?(old_value) + def attribute_change(*) + emit_warning_if_needed("attribute_change", "saved_change_to_attribute") + super end - def attributes_changed_in_place - changed_in_place.each_with_object({}) do |attr_name, h| - orig = @attributes[attr_name].original_value - h[attr_name] = orig - end + def attribute_changed?(*) + emit_warning_if_needed("attribute_changed?", "saved_change_to_attribute?") + super end - def changed_in_place - self.class.attribute_names.select do |attr_name| - attribute_changed_in_place?(attr_name) - end + def changed(*) + emit_warning_if_needed("changed", "saved_changes.keys") + super end - def original_raw_attribute(attr_name) - original_raw_attributes.fetch(attr_name) do - read_attribute_before_type_cast(attr_name) + private + + def mutation_tracker + unless defined?(@mutation_tracker) + @mutation_tracker = nil + end + @mutation_tracker ||= AttributeMutationTracker.new(@attributes) end - end - def original_raw_attributes - @original_raw_attributes ||= {} - end + def emit_warning_if_needed(method_name, new_method_name) + unless mutation_tracker.equal?(mutations_from_database) + ActiveSupport::Deprecation.warn(<<-EOW.squish) + The behavior of `#{method_name}` inside of after callbacks will + be changing in the next version of Rails. The new return value will reflect the + behavior of calling the method after `save` returned (e.g. the opposite of what + it returns now). To maintain the current behavior, use `#{new_method_name}` + instead. + EOW + end + end - def store_original_raw_attribute(attr_name) - original_raw_attributes[attr_name] = @attributes[attr_name].value_for_database rescue nil - end + def mutations_from_database + unless defined?(@mutations_from_database) + @mutations_from_database = nil + end + @mutations_from_database ||= mutation_tracker + end - def store_original_raw_attributes - attribute_names.each do |attr| - store_original_raw_attribute(attr) + def changes_include?(attr_name) + super || mutation_tracker.changed?(attr_name) end - end - def cache_changed_attributes - @cached_changed_attributes = changed_attributes - yield - ensure - clear_changed_attributes_cache - end + def clear_attribute_change(attr_name) + mutation_tracker.forget_change(attr_name) + mutations_from_database.forget_change(attr_name) + end - def clear_changed_attributes_cache - remove_instance_variable(:@cached_changed_attributes) if defined?(@cached_changed_attributes) - end + def attribute_will_change!(attr_name) + super + mutations_from_database.force_change(attr_name) + end + + def _update_record(*) + partial_writes? ? super(keys_for_partial_write) : super + end + + def _create_record(*) + partial_writes? ? super(keys_for_partial_write) : super + end + + def keys_for_partial_write + changed_attribute_names_to_save & self.class.column_names + end + + def forget_attribute_assignments + @attributes = @attributes.map(&:forgetting_assignment) + end + + def clear_mutation_trackers + @mutation_tracker = nil + @mutations_from_database = nil + @mutations_before_last_save = nil + end + + def previous_mutation_tracker + @previous_mutation_tracker ||= NullMutationTracker.instance + end + + def mutations_before_last_save + @mutations_before_last_save ||= previous_mutation_tracker + end + + def cache_changed_attributes + @cached_changed_attributes = changed_attributes + yield + ensure + clear_changed_attributes_cache + end + + def clear_changed_attributes_cache + remove_instance_variable(:@cached_changed_attributes) if defined?(@cached_changed_attributes) + end end end end diff --git a/activerecord/lib/active_record/attribute_methods/primary_key.rb b/activerecord/lib/active_record/attribute_methods/primary_key.rb index c28374e4ab..8fcac82a0d 100644 --- a/activerecord/lib/active_record/attribute_methods/primary_key.rb +++ b/activerecord/lib/active_record/attribute_methods/primary_key.rb @@ -1,11 +1,11 @@ -require 'set' +require "set" module ActiveRecord module AttributeMethods module PrimaryKey extend ActiveSupport::Concern - # Returns this record's primary key value wrapped in an Array if one is + # Returns this record's primary key value wrapped in an array if one is # available. def to_key sync_with_transaction_state @@ -45,84 +45,102 @@ module ActiveRecord attribute_was(self.class.primary_key) end - protected - - def attribute_method?(attr_name) - attr_name == 'id' || super + def id_in_database + sync_with_transaction_state + attribute_in_database(self.class.primary_key) end - module ClassMethods - def define_method_attribute(attr_name) - super + private - if attr_name == primary_key && attr_name != 'id' - generated_attribute_methods.send(:alias_method, :id, primary_key) - end + def attribute_method?(attr_name) + attr_name == "id" || super end - ID_ATTRIBUTE_METHODS = %w(id id= id? id_before_type_cast id_was).to_set + module ClassMethods + def define_method_attribute(attr_name) + super - def dangerous_attribute_method?(method_name) - super && !ID_ATTRIBUTE_METHODS.include?(method_name) - end + if attr_name == primary_key && attr_name != "id" + generated_attribute_methods.send(:alias_method, :id, primary_key) + end + end - # 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 + ID_ATTRIBUTE_METHODS = %w(id id= id? id_before_type_cast id_was id_in_database).to_set - # 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 + def dangerous_attribute_method?(method_name) + super && !ID_ATTRIBUTE_METHODS.include?(method_name) + end - def reset_primary_key #:nodoc: - if self == base_class - self.primary_key = get_primary_key(base_class.name) - else - self.primary_key = base_class.primary_key + # 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. + def quoted_primary_key + @quoted_primary_key ||= connection.quote_column_name(primary_key) end - end - def get_primary_key(base_name) #:nodoc: - if base_name && primary_key_prefix_type == :table_name - base_name.foreign_key(false) - elsif base_name && primary_key_prefix_type == :table_name_with_underscore - base_name.foreign_key - else - if ActiveRecord::Base != self && table_exists? - connection.schema_cache.primary_keys(table_name) + def reset_primary_key #:nodoc: + if self == base_class + self.primary_key = get_primary_key(base_class.name) else - 'id' + self.primary_key = base_class.primary_key end end - end - # Sets the name of the primary key column. - # - # class Project < ActiveRecord::Base - # self.primary_key = 'sysid' - # end - # - # You can also define the +primary_key+ method yourself: - # - # class Project < ActiveRecord::Base - # def self.primary_key - # 'foo_' + super - # end - # end - # - # Project.primary_key # => "foo_id" - def primary_key=(value) - @primary_key = value && value.to_s - @quoted_primary_key = nil - @attributes_builder = nil + def get_primary_key(base_name) #:nodoc: + if base_name && primary_key_prefix_type == :table_name + base_name.foreign_key(false) + elsif base_name && primary_key_prefix_type == :table_name_with_underscore + base_name.foreign_key + else + if ActiveRecord::Base != self && table_exists? + pk = connection.schema_cache.primary_keys(table_name) + suppress_composite_primary_key(pk) + else + "id" + end + end + end + + # Sets the name of the primary key column. + # + # class Project < ActiveRecord::Base + # self.primary_key = 'sysid' + # end + # + # You can also define the #primary_key method yourself: + # + # class Project < ActiveRecord::Base + # def self.primary_key + # 'foo_' + super + # end + # end + # + # Project.primary_key # => "foo_id" + def primary_key=(value) + @primary_key = value && value.to_s + @quoted_primary_key = nil + @attributes_builder = nil + end + + private + + def suppress_composite_primary_key(pk) + return pk unless pk.is_a?(Array) + + warn <<-WARNING.strip_heredoc + WARNING: Active Record does not support composite primary key. + + #{table_name} has composite primary key. Composite primary key is ignored. + WARNING + end end - end end end end diff --git a/activerecord/lib/active_record/attribute_methods/query.rb b/activerecord/lib/active_record/attribute_methods/query.rb index 553122a5fc..10498f4322 100644 --- a/activerecord/lib/active_record/attribute_methods/query.rb +++ b/activerecord/lib/active_record/attribute_methods/query.rb @@ -19,7 +19,7 @@ module ActiveRecord if Numeric === value || value !~ /[^0-9]/ !value.to_i.zero? else - return false if ActiveRecord::ConnectionAdapters::Column::FALSE_VALUES.include?(value) + return false if ActiveModel::Type::Boolean::FALSE_VALUES.include?(value) !value.blank? end elsif value.respond_to?(:zero?) diff --git a/activerecord/lib/active_record/attribute_methods/read.rb b/activerecord/lib/active_record/attribute_methods/read.rb index 0d989c2eca..369a6e35aa 100644 --- a/activerecord/lib/active_record/attribute_methods/read.rb +++ b/activerecord/lib/active_record/attribute_methods/read.rb @@ -1,69 +1,60 @@ module ActiveRecord module AttributeMethods module Read - ReaderMethodCache = Class.new(AttributeMethodCache) { - private - # We want to generate the methods via module_eval rather than - # define_method, because define_method is slower on dispatch. - # Evaluating many similar methods may use more memory as the instruction - # sequences are duplicated and cached (in MRI). define_method may - # be slower on dispatch, but if you're careful about the closure - # created, then define_method will consume much less memory. - # - # But sometimes the database might return columns with - # characters that are not allowed in normal method names (like - # 'my_column(omg)'. So to work around this we first define with - # the __temp__ identifier, and then use alias method to rename - # it to what we want. - # - # We are also defining a constant to hold the frozen string of - # the attribute name. Using a constant means that we do not have - # to allocate an object on each call to the attribute method. - # Making it frozen means that it doesn't get duped when used to - # key the @attributes in read_attribute. - def method_body(method_name, const_name) - <<-EOMETHOD - def #{method_name} - name = ::ActiveRecord::AttributeMethods::AttrNames::ATTR_#{const_name} - _read_attribute(name) { |n| missing_attribute(n, caller) } - end - EOMETHOD - end - }.new - extend ActiveSupport::Concern module ClassMethods - protected + private - def define_method_attribute(name) - safe_name = name.unpack('h*').first - temp_method = "__temp__#{safe_name}" + # We want to generate the methods via module_eval rather than + # define_method, because define_method is slower on dispatch. + # Evaluating many similar methods may use more memory as the instruction + # sequences are duplicated and cached (in MRI). define_method may + # be slower on dispatch, but if you're careful about the closure + # created, then define_method will consume much less memory. + # + # But sometimes the database might return columns with + # characters that are not allowed in normal method names (like + # 'my_column(omg)'. So to work around this we first define with + # the __temp__ identifier, and then use alias method to rename + # it to what we want. + # + # We are also defining a constant to hold the frozen string of + # the attribute name. Using a constant means that we do not have + # to allocate an object on each call to the attribute method. + # Making it frozen means that it doesn't get duped when used to + # key the @attributes in read_attribute. + def define_method_attribute(name) + safe_name = name.unpack("h*".freeze).first + temp_method = "__temp__#{safe_name}" - ActiveRecord::AttributeMethods::AttrNames.set_name_cache safe_name, name + ActiveRecord::AttributeMethods::AttrNames.set_name_cache safe_name, name - generated_attribute_methods.module_eval <<-STR, __FILE__, __LINE__ + 1 - def #{temp_method} - name = ::ActiveRecord::AttributeMethods::AttrNames::ATTR_#{safe_name} - _read_attribute(name) { |n| missing_attribute(n, caller) } - end - STR + generated_attribute_methods.module_eval <<-STR, __FILE__, __LINE__ + 1 + def #{temp_method} + name = ::ActiveRecord::AttributeMethods::AttrNames::ATTR_#{safe_name} + _read_attribute(name) { |n| missing_attribute(n, caller) } + end + STR - generated_attribute_methods.module_eval do - alias_method name, temp_method - undef_method temp_method + generated_attribute_methods.module_eval do + alias_method name, temp_method + undef_method temp_method + end end - end end - ID = 'id'.freeze - # Returns the value of the attribute identified by <tt>attr_name</tt> after # it has been typecast (for example, "2004-12-12" in a date column is cast # to a date object, like Date.new(2004, 12, 12)). def read_attribute(attr_name, &block) - name = attr_name.to_s - name = self.class.primary_key if name == ID + name = if self.class.attribute_alias?(attr_name) + self.class.attribute_alias(attr_name).to_s + else + attr_name.to_s + end + + name = self.class.primary_key if name == "id".freeze _read_attribute(name, &block) end @@ -83,7 +74,6 @@ module ActiveRecord alias :attribute :_read_attribute private :attribute - end end end diff --git a/activerecord/lib/active_record/attribute_methods/serialization.rb b/activerecord/lib/active_record/attribute_methods/serialization.rb index e03bf5945d..945192fe04 100644 --- a/activerecord/lib/active_record/attribute_methods/serialization.rb +++ b/activerecord/lib/active_record/attribute_methods/serialization.rb @@ -9,15 +9,15 @@ module ActiveRecord # attribute using this method and it will be handled automatically. The # serialization is done through YAML. If +class_name+ is specified, the # serialized object must be of that class on assignment and retrieval. - # Otherwise <tt>SerializationTypeMismatch</tt> will be raised. + # Otherwise SerializationTypeMismatch will be raised. # - # Empty objects as +{}+, in the case of +Hash+, or +[]+, in the case of + # Empty objects as <tt>{}</tt>, in the case of +Hash+, or <tt>[]</tt>, in the case of # +Array+, will always be persisted as null. # # Keep in mind that database adapters handle certain serialization tasks # for you. For instance: +json+ and +jsonb+ types in PostgreSQL will be # converted between JSON object/array syntax and Ruby +Hash+ or +Array+ - # objects transparently. There is no need to use +serialize+ in this + # objects transparently. There is no need to use #serialize in this # case. # # For more complex cases, such as conversion to or from your application @@ -26,7 +26,7 @@ module ActiveRecord # ==== Parameters # # * +attr_name+ - The field name that should be serialized. - # * +class_name_or_coder+ - Optional, a coder object, which responds to `.load` / `.dump` + # * +class_name_or_coder+ - Optional, a coder object, which responds to +.load+ and +.dump+ # or a class name that the object type should be equal to. # # ==== Example @@ -50,12 +50,12 @@ module ActiveRecord # to ensure special objects (e.g. Active Record models) are dumped correctly # using the #as_json hook. coder = if class_name_or_coder == ::JSON - Coders::JSON - elsif [:load, :dump].all? { |x| class_name_or_coder.respond_to?(x) } - class_name_or_coder - else - Coders::YAMLColumn.new(class_name_or_coder) - end + Coders::JSON + elsif [:load, :dump].all? { |x| class_name_or_coder.respond_to?(x) } + class_name_or_coder + else + Coders::YAMLColumn.new(class_name_or_coder) + end decorate_attribute_type(attr_name, :serialize) do |type| Type::Serialized.new(type, coder) 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 f9beb43e4b..df1231ad47 100644 --- a/activerecord/lib/active_record/attribute_methods/time_zone_conversion.rb +++ b/activerecord/lib/active_record/attribute_methods/time_zone_conversion.rb @@ -1,3 +1,5 @@ +require "active_support/core_ext/string/strip" + module ActiveRecord module AttributeMethods module TimeZoneConversion @@ -7,34 +9,48 @@ module ActiveRecord end def cast(value) - if value.is_a?(Array) - value.map { |v| cast(v) } - elsif value.is_a?(Hash) + return if value.nil? + + if value.is_a?(Hash) set_time_zone_without_conversion(super) elsif value.respond_to?(:in_time_zone) begin - user_input_in_time_zone(value) || super + super(user_input_in_time_zone(value)) || super rescue ArgumentError nil end + else + map_avoiding_infinite_recursion(super) { |v| cast(v) } end end private - def convert_time_to_time_zone(value) - if value.is_a?(Array) - value.map { |v| convert_time_to_time_zone(v) } - elsif value.acts_like?(:time) - value.in_time_zone - else - value + def convert_time_to_time_zone(value) + return if value.nil? + + if value.acts_like?(:time) + value.in_time_zone + elsif value.is_a?(::Float) + value + else + map_avoiding_infinite_recursion(value) { |v| convert_time_to_time_zone(v) } + end end - end - def set_time_zone_without_conversion(value) - ::Time.zone.local_to_utc(value).in_time_zone - end + def set_time_zone_without_conversion(value) + ::Time.zone.local_to_utc(value).try(:in_time_zone) if value + end + + def map_avoiding_infinite_recursion(value) + map(value) do |v| + if value.equal?(v) + nil + else + yield(v) + end + end + end end extend ActiveSupport::Concern @@ -47,53 +63,32 @@ module ActiveRecord self.skip_time_zone_conversion_for_attributes = [] class_attribute :time_zone_aware_types, instance_writer: false - self.time_zone_aware_types = [:datetime, :not_explicitly_configured] + self.time_zone_aware_types = [:datetime, :time] end module ClassMethods private - def inherited(subclass) - # We need to apply this decorator here, rather than on module inclusion. The closure - # created by the matcher would otherwise evaluate for `ActiveRecord::Base`, not the - # sub class being decorated. As such, changes to `time_zone_aware_attributes`, or - # `skip_time_zone_conversion_for_attributes` would not be picked up. - subclass.class_eval do - matcher = ->(name, type) { create_time_zone_conversion_attribute?(name, type) } - decorate_matching_attribute_types(matcher, :_time_zone_conversion) do |type| - TimeZoneConverter.new(type) + def inherited(subclass) + super + # We need to apply this decorator here, rather than on module inclusion. The closure + # created by the matcher would otherwise evaluate for `ActiveRecord::Base`, not the + # sub class being decorated. As such, changes to `time_zone_aware_attributes`, or + # `skip_time_zone_conversion_for_attributes` would not be picked up. + subclass.class_eval do + matcher = ->(name, type) { create_time_zone_conversion_attribute?(name, type) } + decorate_matching_attribute_types(matcher, :_time_zone_conversion) do |type| + TimeZoneConverter.new(type) + end end end - super - end - - def create_time_zone_conversion_attribute?(name, cast_type) - enabled_for_column = time_zone_aware_attributes && - !self.skip_time_zone_conversion_for_attributes.include?(name.to_sym) - result = enabled_for_column && - time_zone_aware_types.include?(cast_type.type) - - if enabled_for_column && - !result && - cast_type.type == :time && - time_zone_aware_types.include?(:not_explicitly_configured) - ActiveSupport::Deprecation.warn(<<-MESSAGE) - Time columns will become time zone aware in Rails 5.1. This - still causes `String`s to be parsed as if they were in `Time.zone`, - and `Time`s to be converted to `Time.zone`. - To keep the old behavior, you must add the following to your initializer: + def create_time_zone_conversion_attribute?(name, cast_type) + enabled_for_column = time_zone_aware_attributes && + !skip_time_zone_conversion_for_attributes.include?(name.to_sym) - config.active_record.time_zone_aware_types = [:datetime] - - To silence this deprecation warning, add the following: - - config.active_record.time_zone_aware_types << :time - MESSAGE + enabled_for_column && time_zone_aware_types.include?(cast_type.type) end - - result - end end end end diff --git a/activerecord/lib/active_record/attribute_methods/write.rb b/activerecord/lib/active_record/attribute_methods/write.rb index ab017c7b54..fe0e01db28 100644 --- a/activerecord/lib/active_record/attribute_methods/write.rb +++ b/activerecord/lib/active_record/attribute_methods/write.rb @@ -1,19 +1,6 @@ module ActiveRecord module AttributeMethods module Write - WriterMethodCache = Class.new(AttributeMethodCache) { - private - - def method_body(method_name, const_name) - <<-EOMETHOD - def #{method_name}(value) - name = ::ActiveRecord::AttributeMethods::AttrNames::ATTR_#{const_name} - write_attribute(name, value) - end - EOMETHOD - end - }.new - extend ActiveSupport::Concern included do @@ -21,52 +8,58 @@ module ActiveRecord end module ClassMethods - protected + private - def define_method_attribute=(name) - safe_name = name.unpack('h*').first - ActiveRecord::AttributeMethods::AttrNames.set_name_cache safe_name, name + def define_method_attribute=(name) + safe_name = name.unpack("h*".freeze).first + ActiveRecord::AttributeMethods::AttrNames.set_name_cache safe_name, name - generated_attribute_methods.module_eval <<-STR, __FILE__, __LINE__ + 1 - def __temp__#{safe_name}=(value) - name = ::ActiveRecord::AttributeMethods::AttrNames::ATTR_#{safe_name} - write_attribute(name, value) - end - alias_method #{(name + '=').inspect}, :__temp__#{safe_name}= - undef_method :__temp__#{safe_name}= - STR - end + generated_attribute_methods.module_eval <<-STR, __FILE__, __LINE__ + 1 + def __temp__#{safe_name}=(value) + name = ::ActiveRecord::AttributeMethods::AttrNames::ATTR_#{safe_name} + write_attribute(name, value) + end + alias_method #{(name + '=').inspect}, :__temp__#{safe_name}= + undef_method :__temp__#{safe_name}= + STR + end end # Updates the attribute identified by <tt>attr_name</tt> with the - # specified +value+. Empty strings for fixnum and float columns are + # specified +value+. Empty strings for Integer and Float columns are # turned into +nil+. def write_attribute(attr_name, value) - write_attribute_with_type_cast(attr_name, value, true) + name = if self.class.attribute_alias?(attr_name) + self.class.attribute_alias(attr_name).to_s + else + attr_name.to_s + end + + write_attribute_with_type_cast(name, value, true) end - def raw_write_attribute(attr_name, value) + def raw_write_attribute(attr_name, value) # :nodoc: write_attribute_with_type_cast(attr_name, value, false) end private - # Handle *= for method_missing. - def attribute=(attribute_name, value) - write_attribute(attribute_name, value) - end + # Handle *= for method_missing. + def attribute=(attribute_name, value) + write_attribute(attribute_name, value) + end - def write_attribute_with_type_cast(attr_name, value, should_type_cast) - attr_name = attr_name.to_s - attr_name = self.class.primary_key if attr_name == 'id' && self.class.primary_key + def write_attribute_with_type_cast(attr_name, value, should_type_cast) + attr_name = attr_name.to_s + attr_name = self.class.primary_key if attr_name == "id" && self.class.primary_key - if should_type_cast - @attributes.write_from_user(attr_name, value) - else - @attributes.write_cast_value(attr_name, value) - end + if should_type_cast + @attributes.write_from_user(attr_name, value) + else + @attributes.write_cast_value(attr_name, value) + end - value - end + value + end end end end diff --git a/activerecord/lib/active_record/attribute_mutation_tracker.rb b/activerecord/lib/active_record/attribute_mutation_tracker.rb new file mode 100644 index 0000000000..3417090830 --- /dev/null +++ b/activerecord/lib/active_record/attribute_mutation_tracker.rb @@ -0,0 +1,108 @@ +module ActiveRecord + class AttributeMutationTracker # :nodoc: + OPTION_NOT_GIVEN = Object.new + + def initialize(attributes) + @attributes = attributes + @forced_changes = Set.new + end + + def changed_values + attr_names.each_with_object({}.with_indifferent_access) do |attr_name, result| + if changed?(attr_name) + result[attr_name] = attributes[attr_name].original_value + end + end + end + + def changes + attr_names.each_with_object({}.with_indifferent_access) do |attr_name, result| + change = change_to_attribute(attr_name) + if change + result[attr_name] = change + end + end + end + + def change_to_attribute(attr_name) + if changed?(attr_name) + [attributes[attr_name].original_value, attributes.fetch_value(attr_name)] + end + end + + def any_changes? + attr_names.any? { |attr| changed?(attr) } + end + + def changed?(attr_name, from: OPTION_NOT_GIVEN, to: OPTION_NOT_GIVEN) + attr_name = attr_name.to_s + forced_changes.include?(attr_name) || + attributes[attr_name].changed? && + (OPTION_NOT_GIVEN == from || attributes[attr_name].original_value == from) && + (OPTION_NOT_GIVEN == to || attributes[attr_name].value == to) + end + + def changed_in_place?(attr_name) + attributes[attr_name].changed_in_place? + end + + def forget_change(attr_name) + attr_name = attr_name.to_s + attributes[attr_name] = attributes[attr_name].forgetting_assignment + forced_changes.delete(attr_name) + end + + def original_value(attr_name) + attributes[attr_name].original_value + end + + def force_change(attr_name) + forced_changes << attr_name.to_s + end + + # TODO Change this to private once we've dropped Ruby 2.2 support. + # Workaround for Ruby 2.2 "private attribute?" warning. + protected + + attr_reader :attributes, :forced_changes + + private + + def attr_names + attributes.keys + end + end + + class NullMutationTracker # :nodoc: + include Singleton + + def changed_values(*) + {} + end + + def changes(*) + {} + end + + def change_to_attribute(attr_name) + end + + def any_changes?(*) + false + end + + def changed?(*) + false + end + + def changed_in_place?(*) + false + end + + def forget_change(*) + end + + def original_value(*) + end + end +end diff --git a/activerecord/lib/active_record/attribute_set.rb b/activerecord/lib/active_record/attribute_set.rb index 013a7d0e01..66b278219a 100644 --- a/activerecord/lib/active_record/attribute_set.rb +++ b/activerecord/lib/active_record/attribute_set.rb @@ -1,7 +1,10 @@ -require 'active_record/attribute_set/builder' +require "active_record/attribute_set/builder" +require "active_record/attribute_set/yaml_encoder" module ActiveRecord class AttributeSet # :nodoc: + delegate :each_value, :fetch, to: :attributes + def initialize(attributes) @attributes = attributes end @@ -60,8 +63,14 @@ module ActiveRecord super end + def deep_dup + dup.tap do |copy| + copy.instance_variable_set(:@attributes, attributes.deep_dup) + end + end + def initialize_dup(_) - @attributes = attributes.deep_dup + @attributes = attributes.dup super end @@ -80,14 +89,25 @@ module ActiveRecord attributes.select { |_, attr| attr.has_been_read? }.keys end + def map(&block) + new_attributes = attributes.transform_values(&block) + AttributeSet.new(new_attributes) + end + + def ==(other) + attributes == other.attributes + end + + # TODO Change this to private once we've dropped Ruby 2.2 support. + # Workaround for Ruby 2.2 "private attribute?" warning. protected - attr_reader :attributes + attr_reader :attributes private - def initialized_attributes - attributes.select { |_, attr| attr.initialized? } - end + def initialized_attributes + attributes.select { |_, attr| attr.initialized? } + end end end diff --git a/activerecord/lib/active_record/attribute_set/builder.rb b/activerecord/lib/active_record/attribute_set/builder.rb index e85777c335..2f624d32af 100644 --- a/activerecord/lib/active_record/attribute_set/builder.rb +++ b/activerecord/lib/active_record/attribute_set/builder.rb @@ -1,11 +1,14 @@ +require "active_record/attribute" + module ActiveRecord class AttributeSet # :nodoc: class Builder # :nodoc: - attr_reader :types, :always_initialized + attr_reader :types, :always_initialized, :default - def initialize(types, always_initialized = nil) + def initialize(types, always_initialized = nil, &default) @types = types @always_initialized = always_initialized + @default = default end def build_from_database(values = {}, additional_types = {}) @@ -13,21 +16,22 @@ module ActiveRecord values[always_initialized] = nil end - attributes = LazyAttributeHash.new(types, values, additional_types) + attributes = LazyAttributeHash.new(types, values, additional_types, &default) AttributeSet.new(attributes) end end end class LazyAttributeHash # :nodoc: - delegate :transform_values, :each_key, to: :materialize + delegate :transform_values, :each_key, :each_value, :fetch, to: :materialize - def initialize(types, values, additional_types) + def initialize(types, values, additional_types, &default) @types = types @values = values @additional_types = additional_types @materialized = false @delegate_hash = {} + @default = default || proc {} end def key?(key) @@ -45,8 +49,14 @@ module ActiveRecord delegate_hash[key] = value end + def deep_dup + dup.tap do |copy| + copy.instance_variable_set(:@delegate_hash, delegate_hash.transform_values(&:dup)) + end + end + def initialize_dup(_) - @delegate_hash = delegate_hash.transform_values(&:dup) + @delegate_hash = Hash[delegate_hash] super end @@ -60,33 +70,55 @@ module ActiveRecord end end - protected + def ==(other) + if other.is_a?(LazyAttributeHash) + materialize == other.materialize + else + materialize == other + end + end - attr_reader :types, :values, :additional_types, :delegate_hash + def marshal_dump + materialize + end - private + def marshal_load(delegate_hash) + @delegate_hash = delegate_hash + @types = {} + @values = {} + @additional_types = {} + @materialized = true + end + + # TODO Change this to private once we've dropped Ruby 2.2 support. + # Workaround for Ruby 2.2 "private attribute?" warning. + protected - def assign_default_value(name) - type = additional_types.fetch(name, types[name]) - value_present = true - value = values.fetch(name) { value_present = false } + attr_reader :types, :values, :additional_types, :delegate_hash, :default - if value_present - delegate_hash[name] = Attribute.from_database(name, value, type) - elsif types.key?(name) - delegate_hash[name] = Attribute.uninitialized(name, type) + def materialize + unless @materialized + values.each_key { |key| self[key] } + types.each_key { |key| self[key] } + unless frozen? + @materialized = true + end + end + delegate_hash end - end - def materialize - unless @materialized - values.each_key { |key| self[key] } - types.each_key { |key| self[key] } - unless frozen? - @materialized = true + private + + def assign_default_value(name) + type = additional_types.fetch(name, types[name]) + value_present = true + value = values.fetch(name) { value_present = false } + + if value_present + delegate_hash[name] = Attribute.from_database(name, value, type) + elsif types.key?(name) + delegate_hash[name] = default.call(name) || Attribute.uninitialized(name, type) end end - delegate_hash - end end end diff --git a/activerecord/lib/active_record/attribute_set/yaml_encoder.rb b/activerecord/lib/active_record/attribute_set/yaml_encoder.rb new file mode 100644 index 0000000000..899de14792 --- /dev/null +++ b/activerecord/lib/active_record/attribute_set/yaml_encoder.rb @@ -0,0 +1,41 @@ +module ActiveRecord + class AttributeSet + # Attempts to do more intelligent YAML dumping of an + # ActiveRecord::AttributeSet to reduce the size of the resulting string + class YAMLEncoder # :nodoc: + def initialize(default_types) + @default_types = default_types + end + + def encode(attribute_set, coder) + coder["concise_attributes"] = attribute_set.each_value.map do |attr| + if attr.type.equal?(default_types[attr.name]) + attr.with_type(nil) + else + attr + end + end + end + + def decode(coder) + if coder["attributes"] + coder["attributes"] + else + attributes_hash = Hash[coder["concise_attributes"].map do |attr| + if attr.type.nil? + attr = attr.with_type(default_types[attr.name]) + end + [attr.name, attr] + end] + AttributeSet.new(attributes_hash) + end + end + + # TODO Change this to private once we've dropped Ruby 2.2 support. + # Workaround for Ruby 2.2 "private attribute?" warning. + protected + + attr_reader :default_types + end + end +end diff --git a/activerecord/lib/active_record/attributes.rb b/activerecord/lib/active_record/attributes.rb index 8b2c4c7170..75f5ba3a96 100644 --- a/activerecord/lib/active_record/attributes.rb +++ b/activerecord/lib/active_record/attributes.rb @@ -1,13 +1,10 @@ -require 'active_record/attribute/user_provided_default' +require "active_record/attribute/user_provided_default" module ActiveRecord # See ActiveRecord::Attributes::ClassMethods for documentation module Attributes extend ActiveSupport::Concern - # :nodoc: - Type = ActiveRecord::Type - included do class_attribute :attributes_to_define_after_schema_loads, instance_accessor: false # :internal: self.attributes_to_define_after_schema_loads = {} @@ -18,7 +15,7 @@ module ActiveRecord # type of existing attributes if needed. This allows control over how # values are converted to and from SQL when assigned to a model. It also # changes the behavior of values passed to - # ActiveRecord::QueryMethods#where. This will let you use + # {ActiveRecord::Base.where}[rdoc-ref:QueryMethods#where]. This will let you use # your domain objects across much of Active Record, without having to # rely on implementation details or monkey patching. # @@ -37,10 +34,10 @@ module ActiveRecord # is not passed, the previous default value (if any) will be used. # Otherwise, the default will be +nil+. # - # +array+ (PG only) specifies that the type should be an array (see the + # +array+ (PostgreSQL only) specifies that the type should be an array (see the # examples below). # - # +range+ (PG only) specifies that the type should be a range (see the + # +range+ (PostgreSQL only) specifies that the type should be a range (see the # examples below). # # ==== Examples @@ -70,12 +67,14 @@ module ActiveRecord # # A default can also be provided. # + # # db/schema.rb # create_table :store_listings, force: true do |t| # t.string :my_string, default: "original default" # end # # StoreListing.new.my_string # => "original default" # + # # app/models/store_listing.rb # class StoreListing < ActiveRecord::Base # attribute :my_string, :string, default: "new default" # end @@ -90,8 +89,9 @@ module ActiveRecord # sleep 1 # Product.new.my_default_proc # => 2015-05-30 11:04:49 -0600 # - # Attributes do not need to be backed by a database column. + # \Attributes do not need to be backed by a database column. # + # # app/models/my_model.rb # class MyModel < ActiveRecord::Base # attribute :my_string, :string # attribute :my_int_array, :integer, array: true @@ -116,13 +116,13 @@ module ActiveRecord # Users may also define their own custom types, as long as they respond # to the methods defined on the value type. The method +deserialize+ or # +cast+ will be called on your type object, with raw input from the - # database or from your controllers. See ActiveRecord::Type::Value for the + # database or from your controllers. See ActiveModel::Type::Value for the # expected API. It is recommended that your type objects inherit from an # existing type, or from ActiveRecord::Type::Value # # class MoneyType < ActiveRecord::Type::Integer # def cast(value) - # if value.include?('$') + # if !value.kind_of?(Numeric) && value.include?('$') # price_in_dollars = value.gsub(/\$/, '').to_f # super(price_in_dollars * 100) # else @@ -134,7 +134,7 @@ module ActiveRecord # # config/initializers/types.rb # ActiveRecord::Type.register(:money, MoneyType) # - # # /app/models/store_listing.rb + # # app/models/store_listing.rb # class StoreListing < ActiveRecord::Base # attribute :price_in_cents, :money # end @@ -143,13 +143,13 @@ module ActiveRecord # store_listing.price_in_cents # => 1000 # # For more details on creating custom types, see the documentation for - # ActiveRecord::Type::Value. For more details on registering your types + # ActiveModel::Type::Value. For more details on registering your types # to be referenced by a symbol, see ActiveRecord::Type.register. You can # also pass a type object directly, in place of a symbol. # - # ==== Querying + # ==== \Querying # - # When ActiveRecord::QueryMethods#where is called, it will + # When {ActiveRecord::Base.where}[rdoc-ref:QueryMethods#where] is called, it will # use the type defined by the model class to convert the value to SQL, # calling +serialize+ on your type object. For example: # @@ -157,7 +157,7 @@ module ActiveRecord # end # # class MoneyType < Type::Value - # def initialize(currency_converter) + # def initialize(currency_converter:) # @currency_converter = currency_converter # end # @@ -170,11 +170,13 @@ module ActiveRecord # end # end # + # # config/initializers/types.rb # ActiveRecord::Type.register(:money, MoneyType) # + # # app/models/product.rb # class Product < ActiveRecord::Base # currency_converter = ConversionRatesFromTheInternet.new - # attribute :price_in_bitcoins, :money, currency_converter + # attribute :price_in_bitcoins, :money, currency_converter: currency_converter # end # # Product.where(price_in_bitcoins: Money.new(5, "USD")) @@ -188,8 +190,8 @@ module ActiveRecord # The type of an attribute is given the opportunity to change how dirty # tracking is performed. The methods +changed?+ and +changed_in_place?+ # will be called from ActiveModel::Dirty. See the documentation for those - # methods in ActiveRecord::Type::Value for more details. - def attribute(name, cast_type, **options) + # methods in ActiveModel::Type::Value for more details. + def attribute(name, cast_type = Type::Value.new, **options) name = name.to_s reload_schema_from_cache @@ -240,24 +242,24 @@ module ActiveRecord private - NO_DEFAULT_PROVIDED = Object.new # :nodoc: - private_constant :NO_DEFAULT_PROVIDED + NO_DEFAULT_PROVIDED = Object.new # :nodoc: + private_constant :NO_DEFAULT_PROVIDED - def define_default_attribute(name, value, type, from_user:) - if value == NO_DEFAULT_PROVIDED - default_attribute = _default_attributes[name].with_type(type) - elsif from_user - default_attribute = Attribute::UserProvidedDefault.new( - name, - value, - type, - _default_attributes[name], - ) - else - default_attribute = Attribute.from_database(name, value, type) + def define_default_attribute(name, value, type, from_user:) + if value == NO_DEFAULT_PROVIDED + default_attribute = _default_attributes[name].with_type(type) + elsif from_user + default_attribute = Attribute::UserProvidedDefault.new( + name, + value, + type, + _default_attributes.fetch(name.to_s) { nil }, + ) + else + default_attribute = Attribute.from_database(name, value, type) + end + _default_attributes[name] = default_attribute end - _default_attributes[name] = default_attribute - end end end end diff --git a/activerecord/lib/active_record/autosave_association.rb b/activerecord/lib/active_record/autosave_association.rb index 0792d19c3e..9d0b501862 100644 --- a/activerecord/lib/active_record/autosave_association.rb +++ b/activerecord/lib/active_record/autosave_association.rb @@ -1,10 +1,10 @@ module ActiveRecord # = Active Record Autosave Association # - # +AutosaveAssociation+ is a module that takes care of automatically saving + # AutosaveAssociation is a module that takes care of automatically saving # associated records when their parent is saved. In addition to saving, it # also destroys any associated records that were marked for destruction. - # (See +mark_for_destruction+ and <tt>marked_for_destruction?</tt>). + # (See #mark_for_destruction and #marked_for_destruction?). # # Saving of the parent, its associations, and the destruction of marked # associations, all happen inside a transaction. This should never leave the @@ -22,7 +22,7 @@ module ActiveRecord # # == Validation # - # Children records are validated unless <tt>:validate</tt> is +false+. + # Child records are validated unless <tt>:validate</tt> is +false+. # # == Callbacks # @@ -125,7 +125,6 @@ module ActiveRecord # Now it _is_ removed from the database: # # Comment.find_by(id: id).nil? # => true - module AutosaveAssociation extend ActiveSupport::Concern @@ -141,9 +140,11 @@ module ActiveRecord included do Associations::Builder::Association.extensions << AssociationBuilderExtension + mattr_accessor :index_nested_attribute_errors, instance_writer: false + self.index_nested_attribute_errors = false end - module ClassMethods + module ClassMethods # :nodoc: private def define_non_cyclic_method(name, &block) @@ -153,10 +154,10 @@ module ActiveRecord # Loop prevention for validation of associations unless @_already_called[name] begin - @_already_called[name]=true + @_already_called[name] = true result = instance_eval(&block) ensure - @_already_called[name]=false + @_already_called[name] = false end end @@ -222,6 +223,7 @@ module ActiveRecord true end validate validation_method + after_validation :_ensure_no_duplicate_errors end end end @@ -233,7 +235,7 @@ module ActiveRecord super end - # Marks this record to be destroyed as part of the parents save transaction. + # Marks this record to be destroyed as part of the parent's save transaction. # This does _not_ actually destroy the record instantly, rather child record will be destroyed # when <tt>parent.save</tt> is called. # @@ -242,7 +244,7 @@ module ActiveRecord @marked_for_destruction = true end - # Returns whether or not this record will be destroyed as part of the parents save transaction. + # Returns whether or not this record will be destroyed as part of the parent's save transaction. # # Only useful if the <tt>:autosave</tt> option on the parent is enabled for this associated model. def marked_for_destruction? @@ -265,7 +267,7 @@ module ActiveRecord # Returns whether or not this record has been changed in any way (including whether # any of its nested autosave associations are likewise changed) def changed_for_autosave? - new_record? || changed? || marked_for_destruction? || nested_records_changed_for_autosave? + new_record? || has_changes_to_save? || marked_for_destruction? || nested_records_changed_for_autosave? end private @@ -315,7 +317,7 @@ module ActiveRecord def validate_collection_association(reflection) if association = association_instance_get(reflection.name) if records = associated_records_to_validate_or_save(association, new_record?, reflection.options[:autosave]) - records.each { |record| association_valid?(reflection, record) } + records.each_with_index { |record, index| association_valid?(reflection, record, index) } end end end @@ -323,17 +325,30 @@ module ActiveRecord # Returns whether or not the association is valid and applies any errors to # the parent, <tt>self</tt>, if it wasn't. Skips any <tt>:autosave</tt> # enabled records if they're marked_for_destruction? or destroyed. - def association_valid?(reflection, record) - return true if record.destroyed? || record.marked_for_destruction? + def association_valid?(reflection, record, index = nil) + return true if record.destroyed? || (reflection.options[:autosave] && record.marked_for_destruction?) validation_context = self.validation_context unless [:create, :update].include?(self.validation_context) + unless valid = record.valid?(validation_context) if reflection.options[:autosave] + indexed_attribute = !index.nil? && (reflection.options[:index_errors] || ActiveRecord::Base.index_nested_attribute_errors) + record.errors.each do |attribute, message| - attribute = "#{reflection.name}.#{attribute}" + attribute = normalize_reflection_attribute(indexed_attribute, reflection, index, attribute) errors[attribute] << message errors[attribute].uniq! end + + record.errors.details.each_key do |attribute| + reflection_attribute = + normalize_reflection_attribute(indexed_attribute, reflection, index, attribute).to_sym + + record.errors.details[attribute].each do |error| + errors.details[reflection_attribute] << error + errors.details[reflection_attribute].uniq! + end + end else errors.add(reflection.name) end @@ -341,6 +356,14 @@ module ActiveRecord valid end + def normalize_reflection_attribute(indexed_attribute, reflection, index, attribute) + if indexed_attribute + "#{reflection.name}[#{index}].#{attribute}" + else + "#{reflection.name}.#{attribute}" + end + end + # Is used as a before_save callback to check while saving a collection # association whether or not the parent was a new record before saving. def before_save_collection_association @@ -352,7 +375,7 @@ module ActiveRecord # <tt>:autosave</tt> is enabled on the association. # # In addition, it destroys all children that were marked for destruction - # with mark_for_destruction. + # with #mark_for_destruction. # # This all happens inside a transaction, _if_ the Transactions module is included into # ActiveRecord::Base after the AutosaveAssociation module, which it does by default. @@ -360,6 +383,9 @@ module ActiveRecord if association = association_instance_get(reflection.name) autosave = reflection.options[:autosave] + # reconstruct the scope now that we know the owner's id + association.reset_scope if association.respond_to?(:reset_scope) + if records = associated_records_to_validate_or_save(association, @new_record_before_save, autosave) if autosave records_to_destroy = records.select(&:marked_for_destruction?) @@ -379,15 +405,12 @@ module ActiveRecord association.insert_record(record) unless reflection.nested? end elsif autosave - saved = record.save(:validate => false) + saved = record.save(validate: false) end raise ActiveRecord::Rollback unless saved end end - - # reconstruct the scope now that we know the owner's id - association.reset_scope if association.respond_to?(:reset_scope) end end @@ -395,7 +418,7 @@ module ActiveRecord # on the association. # # In addition, it will destroy the association if it was marked for - # destruction with mark_for_destruction. + # destruction with #mark_for_destruction. # # This all happens inside a transaction, _if_ the Transactions module is included into # ActiveRecord::Base after the AutosaveAssociation module, which it does by default. @@ -416,7 +439,7 @@ module ActiveRecord record[reflection.foreign_key] = key end - saved = record.save(:validate => !autosave) + saved = record.save(validate: !autosave) raise ActiveRecord::Rollback if !saved && autosave saved end @@ -428,7 +451,7 @@ module ActiveRecord def record_changed?(reflection, record, key) record.new_record? || (record.has_attribute?(reflection.foreign_key) && record[reflection.foreign_key] != key) || - record.attribute_changed?(reflection.foreign_key) + record.will_save_change_to_attribute?(reflection.foreign_key) end # Saves the associated record if it's new or <tt>:autosave</tt> is enabled. @@ -436,7 +459,9 @@ module ActiveRecord # In addition, it will destroy the association if it was marked for destruction. def save_belongs_to_association(reflection) association = association_instance_get(reflection.name) - record = association && association.load_target + return unless association && association.loaded? && !association.stale_target? + + record = association.load_target if record && !record.destroyed? autosave = reflection.options[:autosave] @@ -444,7 +469,7 @@ module ActiveRecord self[reflection.foreign_key] = nil record.destroy elsif autosave != false - saved = record.save(:validate => !autosave) if record.new_record? || (autosave && record.changed_for_autosave?) + saved = record.save(validate: !autosave) if record.new_record? || (autosave && record.changed_for_autosave?) if association.updated? association_id = record.send(reflection.options[:primary_key] || :id) @@ -456,5 +481,11 @@ module ActiveRecord end end end + + def _ensure_no_duplicate_errors + errors.messages.each_key do |attribute| + errors[attribute].uniq! + end + end end end diff --git a/activerecord/lib/active_record/base.rb b/activerecord/lib/active_record/base.rb index c918e88590..ac1aa2df45 100644 --- a/activerecord/lib/active_record/base.rb +++ b/activerecord/lib/active_record/base.rb @@ -1,27 +1,26 @@ -require 'yaml' -require 'set' -require 'active_support/benchmarkable' -require 'active_support/dependencies' -require 'active_support/descendants_tracker' -require 'active_support/time' -require 'active_support/core_ext/module/attribute_accessors' -require 'active_support/core_ext/array/extract_options' -require 'active_support/core_ext/hash/deep_merge' -require 'active_support/core_ext/hash/slice' -require 'active_support/core_ext/hash/transform_values' -require 'active_support/core_ext/string/behavior' -require 'active_support/core_ext/kernel/singleton_class' -require 'active_support/core_ext/module/introspection' -require 'active_support/core_ext/object/duplicable' -require 'active_support/core_ext/class/subclasses' -require 'arel' -require 'active_record/attribute_decorators' -require 'active_record/errors' -require 'active_record/log_subscriber' -require 'active_record/explain_subscriber' -require 'active_record/relation/delegation' -require 'active_record/attributes' -require 'active_record/type_caster' +require "yaml" +require "active_support/benchmarkable" +require "active_support/dependencies" +require "active_support/descendants_tracker" +require "active_support/time" +require "active_support/core_ext/module/attribute_accessors" +require "active_support/core_ext/array/extract_options" +require "active_support/core_ext/hash/deep_merge" +require "active_support/core_ext/hash/slice" +require "active_support/core_ext/hash/transform_values" +require "active_support/core_ext/string/behavior" +require "active_support/core_ext/kernel/singleton_class" +require "active_support/core_ext/module/introspection" +require "active_support/core_ext/object/duplicable" +require "active_support/core_ext/class/subclasses" +require "active_record/attribute_decorators" +require "active_record/define_callbacks" +require "active_record/errors" +require "active_record/log_subscriber" +require "active_record/explain_subscriber" +require "active_record/relation/delegation" +require "active_record/attributes" +require "active_record/type_caster" module ActiveRecord #:nodoc: # = Active Record @@ -133,9 +132,6 @@ module ActiveRecord #:nodoc: # end # end # - # You can alternatively use <tt>self[:attribute]=(value)</tt> and <tt>self[:attribute]</tt> - # or <tt>write_attribute(:attribute, value)</tt> and <tt>read_attribute(:attribute)</tt>. - # # == Attribute query methods # # In addition to the basic accessors, query methods are also automatically available on the Active Record object. @@ -171,10 +167,11 @@ module ActiveRecord #:nodoc: # <tt>Person.find_by_user_name(user_name)</tt>. # # It's possible to add an exclamation point (!) on the end of the dynamic finders to get them to raise an - # <tt>ActiveRecord::RecordNotFound</tt> error if they do not return any records, + # ActiveRecord::RecordNotFound error if they do not return any records, # like <tt>Person.find_by_last_name!</tt>. # - # It's also possible to use multiple attributes in the same find by separating them with "_and_". + # It's also possible to use multiple attributes in the same <tt>find_by_</tt> by separating them with + # "_and_". # # Person.find_by(user_name: user_name, password: password) # Person.find_by_user_name_and_password(user_name, password) # with dynamic finder @@ -186,7 +183,8 @@ module ActiveRecord #:nodoc: # == Saving arrays, hashes, and other non-mappable objects in text columns # # Active Record can serialize any object in text columns using YAML. To do so, you must - # specify this with a call to the class method +serialize+. + # specify this with a call to the class method + # {serialize}[rdoc-ref:AttributeMethods::Serialization::ClassMethods#serialize]. # This makes it possible to store arrays, hashes, and other non-mappable objects without doing # any additional work. # @@ -226,39 +224,47 @@ module ActiveRecord #:nodoc: # # == Connection to multiple databases in different models # - # Connections are usually created through ActiveRecord::Base.establish_connection and retrieved + # Connections are usually created through + # {ActiveRecord::Base.establish_connection}[rdoc-ref:ConnectionHandling#establish_connection] and retrieved # by ActiveRecord::Base.connection. All classes inheriting from ActiveRecord::Base will use this # connection. But you can also set a class-specific connection. For example, if Course is an # ActiveRecord::Base, but resides in a different database, you can just say <tt>Course.establish_connection</tt> # and Course and all of its subclasses will use this connection instead. # # This feature is implemented by keeping a connection pool in ActiveRecord::Base that is - # a Hash indexed by the class. If a connection is requested, the retrieve_connection method + # a hash indexed by the class. If a connection is requested, the + # {ActiveRecord::Base.retrieve_connection}[rdoc-ref:ConnectionHandling#retrieve_connection] method # will go up the class-hierarchy until a connection is found in the connection pool. # # == Exceptions # # * ActiveRecordError - Generic error class and superclass of all other errors raised by Active Record. - # * AdapterNotSpecified - The configuration hash used in <tt>establish_connection</tt> didn't include an - # <tt>:adapter</tt> key. - # * AdapterNotFound - The <tt>:adapter</tt> key used in <tt>establish_connection</tt> specified a - # non-existent adapter + # * AdapterNotSpecified - The configuration hash used in + # {ActiveRecord::Base.establish_connection}[rdoc-ref:ConnectionHandling#establish_connection] + # didn't include an <tt>:adapter</tt> key. + # * AdapterNotFound - The <tt>:adapter</tt> key used in + # {ActiveRecord::Base.establish_connection}[rdoc-ref:ConnectionHandling#establish_connection] + # specified a non-existent adapter # (or a bad spelling of an existing one). # * AssociationTypeMismatch - The object assigned to the association wasn't of the type # specified in the association definition. # * AttributeAssignmentError - An error occurred while doing a mass assignment through the - # <tt>attributes=</tt> method. + # {ActiveRecord::Base#attributes=}[rdoc-ref:AttributeAssignment#attributes=] method. # You can inspect the +attribute+ property of the exception object to determine which attribute # triggered the error. - # * ConnectionNotEstablished - No connection has been established. Use <tt>establish_connection</tt> - # before querying. + # * ConnectionNotEstablished - No connection has been established. + # Use {ActiveRecord::Base.establish_connection}[rdoc-ref:ConnectionHandling#establish_connection] before querying. # * MultiparameterAssignmentErrors - Collection of errors that occurred during a mass assignment using the - # <tt>attributes=</tt> method. The +errors+ property of this exception contains an array of + # {ActiveRecord::Base#attributes=}[rdoc-ref:AttributeAssignment#attributes=] method. + # The +errors+ property of this exception contains an array of # AttributeAssignmentError # objects that should be inspected to determine which attributes triggered the errors. - # * RecordInvalid - raised by <tt>save!</tt> and <tt>create!</tt> when the record is invalid. - # * RecordNotFound - No record responded to the +find+ method. Either the row with the given ID doesn't exist - # or the row didn't meet the additional restrictions. Some +find+ calls do not raise this exception to signal + # * RecordInvalid - raised by {ActiveRecord::Base#save!}[rdoc-ref:Persistence#save!] and + # {ActiveRecord::Base.create!}[rdoc-ref:Persistence::ClassMethods#create!] + # when the record is invalid. + # * RecordNotFound - No record responded to the {ActiveRecord::Base.find}[rdoc-ref:FinderMethods#find] method. + # Either the row with the given ID doesn't exist or the row didn't meet the additional restrictions. + # Some {ActiveRecord::Base.find}[rdoc-ref:FinderMethods#find] calls do not raise this exception to signal # nothing was found, please check its documentation for further details. # * SerializationTypeMismatch - The serialized object wasn't of the class specified as the second parameter. # * StatementInvalid - The database server rejected the SQL statement. The precise error is added in the message. @@ -280,6 +286,7 @@ module ActiveRecord #:nodoc: extend Explain extend Enum extend Delegation::DelegateCache + extend CollectionCacheKey include Core include Persistence @@ -297,6 +304,7 @@ module ActiveRecord #:nodoc: include AttributeDecorators include Locking::Optimistic include Locking::Pessimistic + include DefineCallbacks include AttributeMethods include Callbacks include Timestamp @@ -306,8 +314,8 @@ module ActiveRecord #:nodoc: include NestedAttributes include Aggregations include Transactions - include NoTouching include TouchLater + include NoTouching include Reflection include Serialization include Store diff --git a/activerecord/lib/active_record/callbacks.rb b/activerecord/lib/active_record/callbacks.rb index 3027ce928e..be6720ddf3 100644 --- a/activerecord/lib/active_record/callbacks.rb +++ b/activerecord/lib/active_record/callbacks.rb @@ -1,11 +1,11 @@ module ActiveRecord - # = Active Record Callbacks + # = Active Record \Callbacks # - # Callbacks are hooks into the life cycle of an Active Record object that allow you to trigger logic + # \Callbacks are hooks into the life cycle of an Active Record object that allow you to trigger logic # before or after an alteration of the object state. This can be used to make sure that associated and - # dependent objects are deleted when +destroy+ is called (by overwriting +before_destroy+) or to massage attributes - # before they're validated (by overwriting +before_validation+). As an example of the callbacks initiated, consider - # the <tt>Base#save</tt> call for a new record: + # dependent objects are deleted when {ActiveRecord::Base#destroy}[rdoc-ref:Persistence#destroy] is called (by overwriting +before_destroy+) or + # to massage attributes before they're validated (by overwriting +before_validation+). + # As an example of the callbacks initiated, consider the {ActiveRecord::Base#save}[rdoc-ref:Persistence#save] call for a new record: # # * (-) <tt>save</tt> # * (-) <tt>valid</tt> @@ -20,7 +20,7 @@ module ActiveRecord # * (7) <tt>after_commit</tt> # # Also, an <tt>after_rollback</tt> callback can be configured to be triggered whenever a rollback is issued. - # Check out <tt>ActiveRecord::Transactions</tt> for more details about <tt>after_commit</tt> and + # Check out ActiveRecord::Transactions for more details about <tt>after_commit</tt> and # <tt>after_rollback</tt>. # # Additionally, an <tt>after_touch</tt> callback is triggered whenever an @@ -31,7 +31,7 @@ module ActiveRecord # are instantiated as well. # # There are nineteen callbacks in total, which give you immense power to react and prepare for each state in the - # Active Record life cycle. The sequence for calling <tt>Base#save</tt> for an existing record is similar, + # Active Record life cycle. The sequence for calling {ActiveRecord::Base#save}[rdoc-ref:Persistence#save] for an existing record is similar, # except that each <tt>_create</tt> callback is replaced by the corresponding <tt>_update</tt> callback. # # Examples: @@ -53,9 +53,9 @@ module ActiveRecord # end # # class Firm < ActiveRecord::Base - # # Destroys the associated clients and people when the firm is destroyed - # before_destroy { |record| Person.destroy_all "firm_id = #{record.id}" } - # before_destroy { |record| Client.destroy_all "client_of = #{record.id}" } + # # Disables access to the system, for associated clients and people when the firm is destroyed + # before_destroy { |record| Person.where(firm_id: record.id).update_all(access: 'disabled') } + # before_destroy { |record| Client.where(client_of: record.id).update_all(access: 'disabled') } # end # # == Inheritable callback queues @@ -175,26 +175,12 @@ module ActiveRecord # end # end # - # The callback macros usually accept a symbol for the method they're supposed to run, but you can also - # pass a "method string", which will then be evaluated within the binding of the callback. Example: - # - # class Topic < ActiveRecord::Base - # before_destroy 'self.class.delete_all "parent_id = #{id}"' - # end - # - # Notice that single quotes (') are used so the <tt>#{id}</tt> part isn't evaluated until the callback - # is triggered. Also note that these inline callbacks can be stacked just like the regular ones: - # - # class Topic < ActiveRecord::Base - # before_destroy 'self.class.delete_all "parent_id = #{id}"', - # 'puts "Evaluated after parents are destroyed"' - # end - # # == <tt>before_validation*</tt> returning statements # # If the +before_validation+ callback throws +:abort+, the process will be - # aborted and <tt>Base#save</tt> will return +false+. If Base#save! is called it will raise a - # <tt>ActiveRecord::RecordInvalid</tt> exception. Nothing will be appended to the errors object. + # aborted and {ActiveRecord::Base#save}[rdoc-ref:Persistence#save] will return +false+. + # If {ActiveRecord::Base#save!}[rdoc-ref:Persistence#save!] is called it will raise an ActiveRecord::RecordInvalid exception. + # Nothing will be appended to the errors object. # # == Canceling callbacks # @@ -206,12 +192,13 @@ module ActiveRecord # == 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. + # callback (+log_children+ in this case) should be executed before the children get destroyed by the + # <tt>dependent: :destroy</tt> option. # # Let's look at the code below: # # class Topic < ActiveRecord::Base - # has_many :children, dependent: destroy + # has_many :children, dependent: :destroy # # before_destroy :log_children # @@ -222,10 +209,11 @@ module ActiveRecord # end # # In this case, the problem is that when the +before_destroy+ callback is executed, the children are not available - # because the +destroy+ callback gets executed first. You can use the +prepend+ option on the +before_destroy+ callback to avoid this. + # because the {ActiveRecord::Base#destroy}[rdoc-ref:Persistence#destroy] callback gets executed first. + # You can use the +prepend+ option on the +before_destroy+ callback to avoid this. # # class Topic < ActiveRecord::Base - # has_many :children, dependent: destroy + # has_many :children, dependent: :destroy # # before_destroy :log_children, prepend: true # @@ -235,23 +223,23 @@ module ActiveRecord # end # end # - # This way, the +before_destroy+ gets executed before the <tt>dependent: destroy</tt> is called, and the data is still available. + # This way, the +before_destroy+ gets executed before the <tt>dependent: :destroy</tt> is called, and the data is still available. # - # == Transactions + # == \Transactions # - # The entire callback chain of a +save+, <tt>save!</tt>, or +destroy+ call runs - # within a transaction. That includes <tt>after_*</tt> hooks. If everything - # goes fine a COMMIT is executed once the chain has been completed. + # The entire callback chain of a {#save}[rdoc-ref:Persistence#save], {#save!}[rdoc-ref:Persistence#save!], + # or {#destroy}[rdoc-ref:Persistence#destroy] call runs within a transaction. That includes <tt>after_*</tt> hooks. + # If everything goes fine a COMMIT is executed once the chain has been completed. # # If a <tt>before_*</tt> callback cancels the action a ROLLBACK is issued. You # can also trigger a ROLLBACK raising an exception in any of the callbacks, # including <tt>after_*</tt> hooks. Note, however, that in that case the client - # needs to be aware of it because an ordinary +save+ will raise such exception + # needs to be aware of it because an ordinary {#save}[rdoc-ref:Persistence#save] will raise such exception # instead of quietly returning +false+. # # == Debugging callbacks # - # The callback chain is accessible via the <tt>_*_callbacks</tt> method on an object. ActiveModel Callbacks support + # The callback chain is accessible via the <tt>_*_callbacks</tt> method on an object. Active Model \Callbacks support # <tt>:before</tt>, <tt>:after</tt> and <tt>:around</tt> as values for the <tt>kind</tt> property. The <tt>kind</tt> property # defines what part of the chain the callback runs in. # @@ -277,36 +265,34 @@ module ActiveRecord :before_destroy, :around_destroy, :after_destroy, :after_commit, :after_rollback ] - module ClassMethods - include ActiveModel::Callbacks - end - - included do - include ActiveModel::Validations::Callbacks - - define_model_callbacks :initialize, :find, :touch, :only => :after - define_model_callbacks :save, :create, :update, :destroy - end - def destroy #:nodoc: - run_callbacks(:destroy) { super } + @_destroy_callback_already_called ||= false + return if @_destroy_callback_already_called + @_destroy_callback_already_called = true + _run_destroy_callbacks { super } + rescue RecordNotDestroyed => e + @_association_destroy_exception = e + false + ensure + @_destroy_callback_already_called = false end def touch(*) #:nodoc: - run_callbacks(:touch) { super } + _run_touch_callbacks { super } end private - def create_or_update(*) #:nodoc: - run_callbacks(:save) { super } + + def create_or_update(*) + _run_save_callbacks { super } end - def _create_record #:nodoc: - run_callbacks(:create) { super } + def _create_record + _run_create_callbacks { super } end - def _update_record(*) #:nodoc: - run_callbacks(:update) { super } + def _update_record(*) + _run_update_callbacks { super } end end end diff --git a/activerecord/lib/active_record/coders/json.rb b/activerecord/lib/active_record/coders/json.rb index 75d3bfe625..cb185a881e 100644 --- a/activerecord/lib/active_record/coders/json.rb +++ b/activerecord/lib/active_record/coders/json.rb @@ -6,7 +6,7 @@ module ActiveRecord end def self.load(json) - ActiveSupport::JSON.decode(json) unless json.nil? + ActiveSupport::JSON.decode(json) unless json.blank? end end end diff --git a/activerecord/lib/active_record/coders/yaml_column.rb b/activerecord/lib/active_record/coders/yaml_column.rb index 9ea22ed798..3a04a10fc9 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' +require "yaml" module ActiveRecord module Coders # :nodoc: class YAMLColumn # :nodoc: - attr_accessor :object_class def initialize(object_class = Object) @@ -14,36 +13,37 @@ module ActiveRecord def dump(obj) return if obj.nil? - unless obj.is_a?(object_class) - raise SerializationTypeMismatch, - "Attribute was supposed to be a #{object_class}, but was a #{obj.class}. -- #{obj.inspect}" - end + assert_valid_value(obj) YAML.dump obj end def load(yaml) return object_class.new if object_class != Object && yaml.nil? - return yaml unless yaml.is_a?(String) && yaml =~ /^---/ + return yaml unless yaml.is_a?(String) && /^---/.match?(yaml) obj = YAML.load(yaml) - unless obj.is_a?(object_class) || obj.nil? - raise SerializationTypeMismatch, - "Attribute was supposed to be a #{object_class}, but was a #{obj.class}" - end + assert_valid_value(obj) obj ||= object_class.new if object_class != Object obj end + def assert_valid_value(obj) + unless obj.nil? || obj.is_a?(object_class) + raise SerializationTypeMismatch, + "Attribute was supposed to be a #{object_class}, but was a #{obj.class}. -- #{obj.inspect}" + end + end + private - def check_arity_of_constructor - begin - load(nil) - rescue ArgumentError - raise ArgumentError, "Cannot serialize #{object_class}. Classes passed to `serialize` must have a 0 argument constructor." + def check_arity_of_constructor + begin + load(nil) + rescue ArgumentError + raise ArgumentError, "Cannot serialize #{object_class}. Classes passed to `serialize` must have a 0 argument constructor." + end end - end end end end diff --git a/activerecord/lib/active_record/collection_cache_key.rb b/activerecord/lib/active_record/collection_cache_key.rb new file mode 100644 index 0000000000..43784b70e3 --- /dev/null +++ b/activerecord/lib/active_record/collection_cache_key.rb @@ -0,0 +1,39 @@ +module ActiveRecord + module CollectionCacheKey + def collection_cache_key(collection = all, timestamp_column = :updated_at) # :nodoc: + query_signature = Digest::MD5.hexdigest(collection.to_sql) + key = "#{collection.model_name.cache_key}/query-#{query_signature}" + + if collection.loaded? + size = collection.size + if size > 0 + timestamp = collection.max_by(×tamp_column).public_send(timestamp_column) + end + else + column_type = type_for_attribute(timestamp_column.to_s) + column = "#{connection.quote_table_name(collection.table_name)}.#{connection.quote_column_name(timestamp_column)}" + + query = collection + .unscope(:select) + .select("COUNT(*) AS #{connection.quote_column_name("size")}", "MAX(#{column}) AS timestamp") + .unscope(:order) + result = connection.select_one(query) + + if result.blank? + size = 0 + timestamp = nil + else + size = result["size"] + timestamp = column_type.deserialize(result["timestamp"]) + end + + end + + if timestamp + "#{key}-#{size}-#{timestamp.utc.to_s(cache_timestamp_format)}" + else + "#{key}-#{size}" + end + end + end +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 6535121075..5ec2fc073e 100644 --- a/activerecord/lib/active_record/connection_adapters/abstract/connection_pool.rb +++ b/activerecord/lib/active_record/connection_adapters/abstract/connection_pool.rb @@ -1,6 +1,6 @@ -require 'thread' -require 'thread_safe' -require 'monitor' +require "thread" +require "concurrent/map" +require "monitor" module ActiveRecord # Raised when a connection could not be obtained within the connection @@ -10,8 +10,9 @@ module ActiveRecord end # Raised when a pool was unable to get ahold of all its connections - # to perform a "group" action such as +ConnectionPool#disconnect!+ - # or +ConnectionPool#clear_reloadable_connections!+. + # to perform a "group" action such as + # {ActiveRecord::Base.connection_pool.disconnect!}[rdoc-ref:ConnectionAdapters::ConnectionPool#disconnect!] + # or {ActiveRecord::Base.clear_reloadable_connections!}[rdoc-ref:ConnectionAdapters::ConnectionHandler#clear_reloadable_connections!]. class ExclusiveConnectionTimeoutError < ConnectionTimeoutError end @@ -37,17 +38,18 @@ module ActiveRecord # Connections can be obtained and used from a connection pool in several # ways: # - # 1. Simply use ActiveRecord::Base.connection as with Active Record 2.1 and + # 1. Simply use {ActiveRecord::Base.connection}[rdoc-ref:ConnectionHandling.connection] + # as with Active Record 2.1 and # earlier (pre-connection-pooling). Eventually, when you're done with # the connection(s) and wish it to be returned to the pool, you call - # ActiveRecord::Base.clear_active_connections!. This will be the - # default behavior for Active Record when used in conjunction with + # {ActiveRecord::Base.clear_active_connections!}[rdoc-ref:ConnectionAdapters::ConnectionHandler#clear_active_connections!]. + # This will be the default behavior for Active Record when used in conjunction with # Action Pack's request handling cycle. # 2. Manually check out a connection from the pool with - # ActiveRecord::Base.connection_pool.checkout. You are responsible for + # {ActiveRecord::Base.connection_pool.checkout}[rdoc-ref:#checkout]. You are responsible for # returning this connection to the pool when finished by calling - # ActiveRecord::Base.connection_pool.checkin(connection). - # 3. Use ActiveRecord::Base.connection_pool.with_connection(&block), which + # {ActiveRecord::Base.connection_pool.checkin(connection)}[rdoc-ref:#checkin]. + # 3. Use {ActiveRecord::Base.connection_pool.with_connection(&block)}[rdoc-ref:#with_connection], which # obtains a connection, yields it as the sole argument to the block, # and returns it to the pool after the block completes. # @@ -67,12 +69,12 @@ module ActiveRecord # threads, which can occur if a programmer forgets to close a # connection at the end of a thread or a thread dies unexpectedly. # Regardless of this setting, the Reaper will be invoked before every - # blocking wait. (Default nil, which means don't schedule the Reaper). + # blocking wait. (Default +nil+, which means don't schedule the Reaper). # #-- # Synchronization policy: # * all public methods can be called outside +synchronize+ - # * access to these i-vars needs to be in +synchronize+: + # * access to these instance variables needs to be in +synchronize+: # * @connections # * @now_connecting # * private methods that require being called in a +synchronize+ blocks @@ -114,7 +116,7 @@ module ActiveRecord end end - # If +element+ is in the queue, remove and return it, or nil. + # If +element+ is in the queue, remove and return it, or +nil+. def delete(element) synchronize do @queue.delete(element) @@ -133,14 +135,14 @@ module ActiveRecord # If +timeout+ is not given, remove and return the head the # queue if the number of available elements is strictly # greater than the number of threads currently waiting (that - # is, don't jump ahead in line). Otherwise, return nil. + # is, don't jump ahead in line). Otherwise, return +nil+. # # If +timeout+ is given, block if there is no element # available, waiting up to +timeout+ seconds for an element to # become available. # # Raises: - # - ConnectionTimeoutError if +timeout+ is given and no element + # - ActiveRecord::ConnectionTimeoutError if +timeout+ is given and no element # becomes available within +timeout+ seconds, def poll(timeout = nil) synchronize { internal_poll(timeout) } @@ -148,61 +150,61 @@ module ActiveRecord private - def internal_poll(timeout) - no_wait_poll || (timeout && wait_poll(timeout)) - end + def internal_poll(timeout) + no_wait_poll || (timeout && wait_poll(timeout)) + end - def synchronize(&block) - @lock.synchronize(&block) - end + def synchronize(&block) + @lock.synchronize(&block) + end - # Test if the queue currently contains any elements. - def any? - !@queue.empty? - end + # Test if the queue currently contains any elements. + def any? + !@queue.empty? + end - # A thread can remove an element from the queue without - # waiting if and only if the number of currently available - # connections is strictly greater than the number of waiting - # threads. - def can_remove_no_wait? - @queue.size > @num_waiting - end + # A thread can remove an element from the queue without + # waiting if and only if the number of currently available + # connections is strictly greater than the number of waiting + # threads. + def can_remove_no_wait? + @queue.size > @num_waiting + end - # Removes and returns the head of the queue if possible, or nil. - def remove - @queue.shift - end + # Removes and returns the head of the queue if possible, or +nil+. + def remove + @queue.shift + end - # Remove and return the head the queue if the number of - # available elements is strictly greater than the number of - # threads currently waiting. Otherwise, return nil. - def no_wait_poll - remove if can_remove_no_wait? - end + # Remove and return the head the queue if the number of + # available elements is strictly greater than the number of + # threads currently waiting. Otherwise, return +nil+. + def no_wait_poll + remove if can_remove_no_wait? + end - # Waits on the queue up to +timeout+ seconds, then removes and - # returns the head of the queue. - def wait_poll(timeout) - @num_waiting += 1 + # Waits on the queue up to +timeout+ seconds, then removes and + # returns the head of the queue. + def wait_poll(timeout) + @num_waiting += 1 - t0 = Time.now - elapsed = 0 - loop do - @cond.wait(timeout - elapsed) + t0 = Time.now + elapsed = 0 + loop do + @cond.wait(timeout - elapsed) - return remove if any? + return remove if any? - elapsed = Time.now - t0 - if elapsed >= timeout - msg = 'could not obtain a database connection within %0.3f seconds (waited %0.3f seconds)' % - [timeout, elapsed] - raise ConnectionTimeoutError, msg + elapsed = Time.now - t0 + if elapsed >= timeout + msg = "could not obtain a connection from the pool within %0.3f seconds (waited %0.3f seconds); all pooled connections were in use" % + [timeout, elapsed] + raise ConnectionTimeoutError, msg + end end + ensure + @num_waiting -= 1 end - ensure - @num_waiting -= 1 - end end # Adds the ability to turn a basic fair FIFO queue into one @@ -272,15 +274,15 @@ module ActiveRecord include BiasableQueue private - def internal_poll(timeout) - conn = super - conn.lease if conn - conn - end + def internal_poll(timeout) + conn = super + conn.lease if conn + conn + end end # Every +frequency+ seconds, the reaper will call +reap+ on +pool+. - # A reaper instantiated with a nil frequency will never reap the + # A reaper instantiated with a +nil+ frequency will never reap the # connection pool. # # Configure the frequency by setting "reaping_frequency" in your @@ -296,7 +298,7 @@ module ActiveRecord def run return unless frequency Thread.new(frequency, pool) { |t, p| - while true + loop do sleep t p.reap end @@ -305,6 +307,7 @@ module ActiveRecord end include MonitorMixin + include QueryCache::ConnectionPoolConfiguration attr_accessor :automatic_reconnect, :checkout_timeout, :schema_cache attr_reader :spec, :connections, :size, :reaper @@ -327,28 +330,27 @@ module ActiveRecord # default max pool size to 5 @size = (spec.config[:pool] && spec.config[:pool].to_i) || 5 - # The cache of threads mapped to reserved connections, the sole purpose - # of the cache is to speed-up +connection+ method, it is not the authoritative - # registry of which thread owns which connection, that is tracked by - # +connection.owner+ attr on each +connection+ instance. - # The invariant works like this: if there is mapping of +thread => conn+, - # then that +thread+ does indeed own that +conn+, however an absence of a such - # mapping does not mean that the +thread+ doesn't own the said connection, in + # This variable tracks the cache of threads mapped to reserved connections, with the + # sole purpose of speeding up the +connection+ method. It is not the authoritative + # registry of which thread owns which connection. Connection ownership is tracked by + # the +connection.owner+ attr on each +connection+ instance. + # The invariant works like this: if there is mapping of <tt>thread => conn</tt>, + # then that +thread+ does indeed own that +conn+. However, an absence of a such + # mapping does not mean that the +thread+ doesn't own the said connection. In # that case +conn.owner+ attr should be consulted. # Access and modification of +@thread_cached_conns+ does not require # synchronization. - @thread_cached_conns = ThreadSafe::Cache.new(:initial_capacity => @size) + @thread_cached_conns = Concurrent::Map.new(initial_capacity: @size) @connections = [] @automatic_reconnect = true - # Connection pool allows for concurrent (outside the main `synchronize` section) + # Connection pool allows for concurrent (outside the main +synchronize+ section) # establishment of new connections. This variable tracks the number of threads # currently in the process of independently establishing connections to the DB. @now_connecting = 0 - # A boolean toggle that allows/disallows new connections. - @new_cons_enabled = true + @threads_blocking_new_connections = 0 @available = ConnectionLeasingQueue.new self end @@ -362,10 +364,10 @@ module ActiveRecord @thread_cached_conns[connection_cache_key(Thread.current)] ||= checkout end - # Is there an open connection that is being used for the current thread? + # Returns true if there is an open connection being used for the current thread. # - # This method only works for connections that have been abtained through - # #connection or #with_connection methods, connections obtained through + # This method only works for connections that have been obtained through + # #connection or #with_connection methods. Connections obtained through # #checkout will not be detected by #active_connection? def active_connection? @thread_cached_conns[connection_cache_key(Thread.current)] @@ -406,14 +408,17 @@ module ActiveRecord # Disconnects all connections in the pool, and clears the pool. # # Raises: - # - +ExclusiveConnectionTimeoutError+ if unable to gain ownership of all + # - ActiveRecord::ExclusiveConnectionTimeoutError if unable to gain ownership of all # connections in the pool within a timeout interval (default duration is - # +spec.config[:checkout_timeout] * 2+ seconds). + # <tt>spec.config[:checkout_timeout] * 2</tt> seconds). def disconnect(raise_on_acquisition_timeout = true) with_exclusively_acquired_all_connections(raise_on_acquisition_timeout) do synchronize do @connections.each do |conn| - checkin conn + if conn.in_use? + conn.steal! + checkin conn + end conn.disconnect! end @connections = [] @@ -424,10 +429,10 @@ module ActiveRecord # Disconnects all connections in the pool, and clears the pool. # - # The pool first tries to gain ownership of all connections, if unable to + # The pool first tries to gain ownership of all connections. If unable to # do so within a timeout interval (default duration is - # +spec.config[:checkout_timeout] * 2+ seconds), the pool is forcefully - # disconneted wihout any regard for other connection owning threads. + # <tt>spec.config[:checkout_timeout] * 2</tt> seconds), then the pool is forcefully + # disconnected without any regard for other connection owning threads. def disconnect! disconnect(false) end @@ -436,45 +441,31 @@ module ActiveRecord # require reloading. # # Raises: - # - +ExclusiveConnectionTimeoutError+ if unable to gain ownership of all + # - ActiveRecord::ExclusiveConnectionTimeoutError if unable to gain ownership of all # connections in the pool within a timeout interval (default duration is - # +spec.config[:checkout_timeout] * 2+ seconds). + # <tt>spec.config[:checkout_timeout] * 2</tt> seconds). def clear_reloadable_connections(raise_on_acquisition_timeout = true) - num_new_conns_required = 0 - with_exclusively_acquired_all_connections(raise_on_acquisition_timeout) do synchronize do @connections.each do |conn| - checkin conn + if conn.in_use? + conn.steal! + checkin conn + end conn.disconnect! if conn.requires_reloading? end @connections.delete_if(&:requires_reloading?) - @available.clear - - if @connections.size < @size - # because of the pruning done by this method, we might be running - # low on connections, while threads stuck in queue are helpless - # (not being able to establish new connections for themselves), - # see also more detailed explanation in +remove+ - num_new_conns_required = num_waiting_in_queue - @connections.size - end - - @connections.each do |conn| - @available.add conn - end end end - - bulk_make_new_connections(num_new_conns_required) if num_new_conns_required > 0 end # Clears the cache which maps classes and re-connects connections that # require reloading. # - # The pool first tries to gain ownership of all connections, if unable to + # The pool first tries to gain ownership of all connections. If unable to # do so within a timeout interval (default duration is - # +spec.config[:checkout_timeout] * 2+ seconds), the pool forcefully + # <tt>spec.config[:checkout_timeout] * 2</tt> seconds), then the pool forcefully # clears the cache and reloads connections without any regard for other # connection owning threads. def clear_reloadable_connections! @@ -494,7 +485,7 @@ module ActiveRecord # Returns: an AbstractAdapter object. # # Raises: - # - ConnectionTimeoutError: no connection can be obtained from the pool. + # - ActiveRecord::ConnectionTimeoutError no connection can be obtained from the pool. def checkout(checkout_timeout = @checkout_timeout) checkout_and_verify(acquire_connection(checkout_timeout)) end @@ -503,12 +494,12 @@ module ActiveRecord # no longer need this connection. # # +conn+: an AbstractAdapter object, which was obtained by earlier by - # calling +checkout+ on this pool. + # calling #checkout on this pool. def checkin(conn) synchronize do remove_connection_from_thread_cache conn - conn.run_callbacks :checkin do + conn._run_checkin_callbacks do conn.expire end @@ -516,7 +507,7 @@ module ActiveRecord end end - # Remove a connection from the connection pool. The connection will + # Remove a connection from the connection pool. The connection will # remain open and active but will no longer be managed by this pool. def remove(conn) needs_new_connection = false @@ -528,43 +519,43 @@ module ActiveRecord @available.delete conn # @available.any_waiting? => true means that prior to removing this - # conn, the pool was at its max size (@connections.size == @size) - # this would mean that any threads stuck waiting in the queue wouldn't + # conn, the pool was at its max size (@connections.size == @size). + # This would mean that any threads stuck waiting in the queue wouldn't # know they could checkout_new_connection, so let's do it for them. # Because condition-wait loop is encapsulated in the Queue class # (that in turn is oblivious to ConnectionPool implementation), threads - # that are "stuck" there are helpless, they have no way of creating + # that are "stuck" there are helpless. They have no way of creating # new connections and are completely reliant on us feeding available # connections into the Queue. needs_new_connection = @available.any_waiting? end # This is intentionally done outside of the synchronized section as we - # would like not to hold the main mutex while checking out new connections, - # thus there is some chance that needs_new_connection information is now + # would like not to hold the main mutex while checking out new connections. + # Thus there is some chance that needs_new_connection information is now # stale, we can live with that (bulk_make_new_connections will make # sure not to exceed the pool's @size limit). bulk_make_new_connections(1) if needs_new_connection end - # Recover lost connections for the pool. A lost connection can occur if + # Recover lost connections for the pool. A lost connection can occur if # a programmer forgets to checkin a connection at the end of a thread # or a thread dies unexpectedly. def reap stale_connections = synchronize do @connections.select do |conn| conn.in_use? && !conn.owner.alive? + end.each do |conn| + conn.steal! end end stale_connections.each do |conn| - synchronize do - if conn.active? - conn.reset! - checkin conn - else - remove conn - end + if conn.active? + conn.reset! + checkin conn + else + remove conn end end end @@ -573,211 +564,247 @@ module ActiveRecord @available.num_waiting end - private - #-- - # this is unfortunately not concurrent - def bulk_make_new_connections(num_new_conns_needed) - num_new_conns_needed.times do - # try_to_checkout_new_connection will not exceed pool's @size limit - if new_conn = try_to_checkout_new_connection - # make the new_conn available to the starving threads stuck @available Queue - checkin(new_conn) - end + # Return connection pool's usage statistic + # Example: + # + # ActiveRecord::Base.connection_pool.stat # => { size: 15, connections: 1, busy: 1, dead: 0, idle: 0, waiting: 0, checkout_timeout: 5 } + def stat + synchronize do + { + size: size, + connections: @connections.size, + busy: @connections.count { |c| c.in_use? && c.owner.alive? }, + dead: @connections.count { |c| c.in_use? && !c.owner.alive? }, + idle: @connections.count { |c| !c.in_use? }, + waiting: num_waiting_in_queue, + checkout_timeout: checkout_timeout + } end end - #-- - # From the discussion on Github: - # https://github.com/rails/rails/pull/14938#commitcomment-6601951 - # This hook-in method allows for easier monkey-patching fixes needed by - # JRuby users that use Fibers. - def connection_cache_key(thread) - thread - end + private + #-- + # this is unfortunately not concurrent + def bulk_make_new_connections(num_new_conns_needed) + num_new_conns_needed.times do + # try_to_checkout_new_connection will not exceed pool's @size limit + if new_conn = try_to_checkout_new_connection + # make the new_conn available to the starving threads stuck @available Queue + checkin(new_conn) + end + end + end - # Take control of all existing connections so a "group" action such as - # reload/disconnect can be performed safely. It is no longer enough to - # wrap it in +synchronize+ because some pool's actions are allowed - # to be performed outside of the main +synchronize+ block. - def with_exclusively_acquired_all_connections(raise_on_acquisition_timeout = true) - with_new_connections_blocked do - attempt_to_checkout_all_existing_connections(raise_on_acquisition_timeout) - yield + #-- + # From the discussion on GitHub: + # https://github.com/rails/rails/pull/14938#commitcomment-6601951 + # This hook-in method allows for easier monkey-patching fixes needed by + # JRuby users that use Fibers. + def connection_cache_key(thread) + thread end - end - def attempt_to_checkout_all_existing_connections(raise_on_acquisition_timeout = true) - collected_conns = synchronize do - # account for our own connections - @connections.select {|conn| conn.owner == Thread.current} + # Take control of all existing connections so a "group" action such as + # reload/disconnect can be performed safely. It is no longer enough to + # wrap it in +synchronize+ because some pool's actions are allowed + # to be performed outside of the main +synchronize+ block. + def with_exclusively_acquired_all_connections(raise_on_acquisition_timeout = true) + with_new_connections_blocked do + attempt_to_checkout_all_existing_connections(raise_on_acquisition_timeout) + yield + end end - newly_checked_out = [] - timeout_time = Time.now + (@checkout_timeout * 2) + def attempt_to_checkout_all_existing_connections(raise_on_acquisition_timeout = true) + collected_conns = synchronize do + # account for our own connections + @connections.select { |conn| conn.owner == Thread.current } + end - @available.with_a_bias_for(Thread.current) do - while true - synchronize do - return if collected_conns.size == @connections.size && @now_connecting == 0 - remaining_timeout = timeout_time - Time.now - remaining_timeout = 0 if remaining_timeout < 0 - conn = checkout_for_exclusive_access(remaining_timeout) - collected_conns << conn - newly_checked_out << conn + newly_checked_out = [] + timeout_time = Time.now + (@checkout_timeout * 2) + + @available.with_a_bias_for(Thread.current) do + loop do + synchronize do + return if collected_conns.size == @connections.size && @now_connecting == 0 + remaining_timeout = timeout_time - Time.now + remaining_timeout = 0 if remaining_timeout < 0 + conn = checkout_for_exclusive_access(remaining_timeout) + collected_conns << conn + newly_checked_out << conn + end end end - end - rescue ExclusiveConnectionTimeoutError - # `raise_on_acquisition_timeout == false` means we are directed to ignore any - # timeouts and are expected to just give up: we've obtained as many connections - # as possible, note that in a case like that we don't return any of the - # `newly_checked_out` connections. - - if raise_on_acquisition_timeout + rescue ExclusiveConnectionTimeoutError + # <tt>raise_on_acquisition_timeout == false</tt> means we are directed to ignore any + # timeouts and are expected to just give up: we've obtained as many connections + # as possible, note that in a case like that we don't return any of the + # +newly_checked_out+ connections. + + if raise_on_acquisition_timeout + release_newly_checked_out = true + raise + end + rescue Exception # if something else went wrong + # this can't be a "naked" rescue, because we have should return conns + # even for non-StandardErrors release_newly_checked_out = true raise + ensure + if release_newly_checked_out && newly_checked_out + # releasing only those conns that were checked out in this method, conns + # checked outside this method (before it was called) are not for us to release + newly_checked_out.each { |conn| checkin(conn) } + end end - rescue Exception # if something else went wrong - # this can't be a "naked" rescue, because we have should return conns - # even for non-StandardErrors - release_newly_checked_out = true - raise - ensure - if release_newly_checked_out && newly_checked_out - # releasing only those conns that were checked out in this method, conns - # checked outside this method (before it was called) are not for us to release - newly_checked_out.each {|conn| checkin(conn)} - end - end - #-- - # Must be called in a synchronize block. - def checkout_for_exclusive_access(checkout_timeout) - checkout(checkout_timeout) - rescue ConnectionTimeoutError - # this block can't be easily moved into attempt_to_checkout_all_existing_connections's - # rescue block, because doing so would put it outside of synchronize section, without - # being in a critical section thread_report might become inaccurate - msg = "could not obtain ownership of all database connections in #{checkout_timeout} seconds" - - thread_report = [] - @connections.each do |conn| - unless conn.owner == Thread.current - thread_report << "#{conn} is owned by #{conn.owner}" + #-- + # Must be called in a synchronize block. + def checkout_for_exclusive_access(checkout_timeout) + checkout(checkout_timeout) + rescue ConnectionTimeoutError + # this block can't be easily moved into attempt_to_checkout_all_existing_connections's + # rescue block, because doing so would put it outside of synchronize section, without + # being in a critical section thread_report might become inaccurate + msg = "could not obtain ownership of all database connections in #{checkout_timeout} seconds" + + thread_report = [] + @connections.each do |conn| + unless conn.owner == Thread.current + thread_report << "#{conn} is owned by #{conn.owner}" + end end + + msg << " (#{thread_report.join(', ')})" if thread_report.any? + + raise ExclusiveConnectionTimeoutError, msg end - msg << " (#{thread_report.join(', ')})" if thread_report.any? + def with_new_connections_blocked + synchronize do + @threads_blocking_new_connections += 1 + end - raise ExclusiveConnectionTimeoutError, msg - end + yield + ensure + num_new_conns_required = 0 - def with_new_connections_blocked - previous_value = nil - synchronize do - previous_value, @new_cons_enabled = @new_cons_enabled, false + synchronize do + @threads_blocking_new_connections -= 1 + + if @threads_blocking_new_connections.zero? + @available.clear + + num_new_conns_required = num_waiting_in_queue + + @connections.each do |conn| + next if conn.in_use? + + @available.add conn + num_new_conns_required -= 1 + end + end + end + + bulk_make_new_connections(num_new_conns_required) if num_new_conns_required > 0 end - yield - ensure - synchronize { @new_cons_enabled = previous_value } - end - # Acquire a connection by one of 1) immediately removing one - # from the queue of available connections, 2) creating a new - # connection if the pool is not at capacity, 3) waiting on the - # queue for a connection to become available. - # - # Raises: - # - ConnectionTimeoutError if a connection could not be acquired - # - #-- - # Implementation detail: the connection returned by +acquire_connection+ - # will already be "+connection.lease+ -ed" to the current thread. - def acquire_connection(checkout_timeout) - # NOTE: we rely on `@available.poll` and `try_to_checkout_new_connection` to - # `conn.lease` the returned connection (and to do this in a `synchronized` - # section), this is not the cleanest implementation, as ideally we would - # `synchronize { conn.lease }` in this method, but by leaving it to `@available.poll` - # and `try_to_checkout_new_connection` we can piggyback on `synchronize` sections - # of the said methods and avoid an additional `synchronize` overhead. - if conn = @available.poll || try_to_checkout_new_connection - conn - else - reap - @available.poll(checkout_timeout) + # Acquire a connection by one of 1) immediately removing one + # from the queue of available connections, 2) creating a new + # connection if the pool is not at capacity, 3) waiting on the + # queue for a connection to become available. + # + # Raises: + # - ActiveRecord::ConnectionTimeoutError if a connection could not be acquired + # + #-- + # Implementation detail: the connection returned by +acquire_connection+ + # will already be "+connection.lease+ -ed" to the current thread. + def acquire_connection(checkout_timeout) + # NOTE: we rely on +@available.poll+ and +try_to_checkout_new_connection+ to + # +conn.lease+ the returned connection (and to do this in a +synchronized+ + # section). This is not the cleanest implementation, as ideally we would + # <tt>synchronize { conn.lease }</tt> in this method, but by leaving it to +@available.poll+ + # and +try_to_checkout_new_connection+ we can piggyback on +synchronize+ sections + # of the said methods and avoid an additional +synchronize+ overhead. + if conn = @available.poll || try_to_checkout_new_connection + conn + else + reap + @available.poll(checkout_timeout) + end end - end - #-- - # if owner_thread param is omitted, this must be called in synchronize block - def remove_connection_from_thread_cache(conn, owner_thread = conn.owner) - @thread_cached_conns.delete_pair(connection_cache_key(owner_thread), conn) - end - alias_method :release, :remove_connection_from_thread_cache + #-- + # if owner_thread param is omitted, this must be called in synchronize block + def remove_connection_from_thread_cache(conn, owner_thread = conn.owner) + @thread_cached_conns.delete_pair(connection_cache_key(owner_thread), conn) + end + alias_method :release, :remove_connection_from_thread_cache - def new_connection - Base.send(spec.adapter_method, spec.config).tap do |conn| - conn.schema_cache = schema_cache.dup if schema_cache + def new_connection + Base.send(spec.adapter_method, spec.config).tap do |conn| + conn.schema_cache = schema_cache.dup if schema_cache + end end - end - # If the pool is not at a +@size+ limit, establish new connection. Connecting - # to the DB is done outside main synchronized section. - #-- - # Implementation constraint: a newly established connection returned by this - # method must be in the +.leased+ state. - def try_to_checkout_new_connection - # first in synchronized section check if establishing new conns is allowed - # and increment @now_connecting, to prevent overstepping this pool's @size - # constraint - do_checkout = synchronize do - if @new_cons_enabled && (@connections.size + @now_connecting) < @size - @now_connecting += 1 - end - end - if do_checkout - begin - # if successfully incremented @now_connecting establish new connection - # outside of synchronized section - conn = checkout_new_connection - ensure - synchronize do - if conn - adopt_connection(conn) - # returned conn needs to be already leased - conn.lease + # If the pool is not at a +@size+ limit, establish new connection. Connecting + # to the DB is done outside main synchronized section. + #-- + # Implementation constraint: a newly established connection returned by this + # method must be in the +.leased+ state. + def try_to_checkout_new_connection + # first in synchronized section check if establishing new conns is allowed + # and increment @now_connecting, to prevent overstepping this pool's @size + # constraint + do_checkout = synchronize do + if @threads_blocking_new_connections.zero? && (@connections.size + @now_connecting) < @size + @now_connecting += 1 + end + end + if do_checkout + begin + # if successfully incremented @now_connecting establish new connection + # outside of synchronized section + conn = checkout_new_connection + ensure + synchronize do + if conn + adopt_connection(conn) + # returned conn needs to be already leased + conn.lease + end + @now_connecting -= 1 end - @now_connecting -= 1 end end end - end - def adopt_connection(conn) - conn.pool = self - @connections << conn - end + def adopt_connection(conn) + conn.pool = self + @connections << conn + end - def checkout_new_connection - raise ConnectionNotEstablished unless @automatic_reconnect - new_connection - end + def checkout_new_connection + raise ConnectionNotEstablished unless @automatic_reconnect + new_connection + end - def checkout_and_verify(c) - c.run_callbacks :checkout do - c.verify! + def checkout_and_verify(c) + c._run_checkout_callbacks do + c.verify! + end + c + rescue + remove c + c.disconnect! + raise end - c - rescue - remove c - c.disconnect! - raise - end end # ConnectionHandler is a collection of ConnectionPool objects. It is used - # for keeping separate connection pools for Active Record models that connect - # to different databases. + # for keeping separate connection pools that connect to different databases. # # For example, suppose that you have 5 models, with the following hierarchy: # @@ -819,16 +846,15 @@ module ActiveRecord # ConnectionHandler accessible via ActiveRecord::Base.connection_handler. # All Active Record models use this handler to determine the connection pool that they # should use. + # + # The ConnectionHandler class is not coupled with the Active models, as it has no knowlodge + # about the model. The model needs to pass a specification name to the handler, + # in order to lookup the correct connection pool. class ConnectionHandler def initialize - # These caches are keyed by klass.name, NOT klass. Keying them by klass - # alone would lead to memory leaks in development mode as all previous - # instances of the class would stay in memory. - @owner_to_pool = ThreadSafe::Cache.new(:initial_capacity => 2) do |h,k| - h[k] = ThreadSafe::Cache.new(:initial_capacity => 2) - end - @class_to_pool = ThreadSafe::Cache.new(:initial_capacity => 2) do |h,k| - h[k] = ThreadSafe::Cache.new + # These caches are keyed by spec.name (ConnectionSpecification#name). + @owner_to_pool = Concurrent::Map.new(initial_capacity: 2) do |h, k| + h[k] = Concurrent::Map.new(initial_capacity: 2) end end @@ -837,10 +863,26 @@ module ActiveRecord end alias :connection_pools :connection_pool_list - def establish_connection(owner, spec) - @class_to_pool.clear - raise RuntimeError, "Anonymous class is not allowed." unless owner.name - owner_to_pool[owner.name] = ConnectionAdapters::ConnectionPool.new(spec) + def establish_connection(config) + resolver = ConnectionSpecification::Resolver.new(Base.configurations) + spec = resolver.spec(config) + + remove_connection(spec.name) + + message_bus = ActiveSupport::Notifications.instrumenter + payload = { + connection_id: object_id + } + if spec + payload[:spec_name] = spec.name + payload[:config] = spec.config + end + + message_bus.instrument("!connection.active_record", payload) do + owner_to_pool[spec.name] = ConnectionAdapters::ConnectionPool.new(spec) + end + + owner_to_pool[spec.name] end # Returns true if there are any active connections among the connection @@ -857,6 +899,8 @@ module ActiveRecord end # Clears the cache which maps classes. + # + # See ConnectionPool#clear_reloadable_connections! for details. def clear_reloadable_connections! connection_pool_list.each(&:clear_reloadable_connections!) end @@ -869,103 +913,63 @@ module ActiveRecord # active or defined connection: if it is the latter, it will be # opened and set as the active connection for the class it was defined # for (not necessarily the current class). - def retrieve_connection(klass) #:nodoc: - pool = retrieve_connection_pool(klass) - raise ConnectionNotEstablished, "No connection pool for #{klass}" unless pool + def retrieve_connection(spec_name) #:nodoc: + pool = retrieve_connection_pool(spec_name) + raise ConnectionNotEstablished, "No connection pool with '#{spec_name}' found." unless pool conn = pool.connection - raise ConnectionNotEstablished, "No connection for #{klass} in connection pool" unless conn + raise ConnectionNotEstablished, "No connection for '#{spec_name}' in connection pool" unless conn conn end # Returns true if a connection that's accessible to this class has # already been opened. - def connected?(klass) - conn = retrieve_connection_pool(klass) + def connected?(spec_name) + conn = retrieve_connection_pool(spec_name) conn && conn.connected? end # Remove the connection for this class. This will close the active # connection and the defined connection (if they exist). The result - # can be used as an argument for establish_connection, for easily + # can be used as an argument for #establish_connection, for easily # re-establishing the connection. - def remove_connection(owner) - if pool = owner_to_pool.delete(owner.name) - @class_to_pool.clear + def remove_connection(spec_name) + if pool = owner_to_pool.delete(spec_name) pool.automatic_reconnect = false pool.disconnect! pool.spec.config end end - # Retrieving the connection pool happens a lot so we cache it in @class_to_pool. + # Retrieving the connection pool happens a lot, so we cache it in @owner_to_pool. # This makes retrieving the connection pool O(1) once the process is warm. # When a connection is established or removed, we invalidate the cache. - # - # Ideally we would use #fetch here, as class_to_pool[klass] may sometimes be nil. - # However, benchmarking (https://gist.github.com/jonleighton/3552829) showed that - # #fetch is significantly slower than #[]. So in the nil case, no caching will - # take place, but that's ok since the nil case is not the common one that we wish - # to optimise for. - def retrieve_connection_pool(klass) - class_to_pool[klass.name] ||= begin - until pool = pool_for(klass) - klass = klass.superclass - break unless klass <= Base - end - - class_to_pool[klass.name] = pool - end - end - - private - - def owner_to_pool - @owner_to_pool[Process.pid] - end - - def class_to_pool - @class_to_pool[Process.pid] - end - - def pool_for(owner) - owner_to_pool.fetch(owner.name) { - if ancestor_pool = pool_from_any_process_for(owner) + def retrieve_connection_pool(spec_name) + owner_to_pool.fetch(spec_name) do + # Check if a connection was previously established in an ancestor process, + # which may have been forked. + if ancestor_pool = pool_from_any_process_for(spec_name) # A connection was established in an ancestor process that must have # subsequently forked. We can't reuse the connection, but we can copy # the specification and establish a new connection with it. - establish_connection(owner, ancestor_pool.spec).tap do |pool| + establish_connection(ancestor_pool.spec.to_hash).tap do |pool| pool.schema_cache = ancestor_pool.schema_cache if ancestor_pool.schema_cache end else - owner_to_pool[owner.name] = nil + owner_to_pool[spec_name] = nil end - } - end - - def pool_from_any_process_for(owner) - owner_to_pool = @owner_to_pool.values.find { |v| v[owner.name] } - owner_to_pool && owner_to_pool[owner.name] - end - end - - class ConnectionManagement - def initialize(app) - @app = app + end end - def call(env) - testing = env['rack.test'] + private - response = @app.call(env) - response[2] = ::Rack::BodyProxy.new(response[2]) do - ActiveRecord::Base.clear_active_connections! unless testing + def owner_to_pool + @owner_to_pool[Process.pid] end - response - rescue Exception - ActiveRecord::Base.clear_active_connections! unless testing - raise - end + def pool_from_any_process_for(spec_name) + owner_to_pool = @owner_to_pool.values.find { |v| v[spec_name] } + owner_to_pool && owner_to_pool[spec_name] + end end end end diff --git a/activerecord/lib/active_record/connection_adapters/abstract/database_limits.rb b/activerecord/lib/active_record/connection_adapters/abstract/database_limits.rb index 30b2fca2ca..407e019326 100644 --- a/activerecord/lib/active_record/connection_adapters/abstract/database_limits.rb +++ b/activerecord/lib/active_record/connection_adapters/abstract/database_limits.rb @@ -1,7 +1,6 @@ module ActiveRecord module ConnectionAdapters # :nodoc: module DatabaseLimits - # Returns the maximum length of a table alias. def table_alias_length 255 @@ -19,8 +18,8 @@ module ActiveRecord # Returns the maximum allowed length for an index name. This # limit is enforced by \Rails and is less than or equal to - # <tt>index_name_length</tt>. The gap between - # <tt>index_name_length</tt> is to allow internal \Rails + # #index_name_length. The gap between + # #index_name_length is to allow internal \Rails # operations to use prefixes in temporary operations. def allowed_index_name_length index_name_length @@ -47,7 +46,7 @@ module ActiveRecord end # Returns the maximum number of elements in an IN (x,y,z) clause. - # nil means no limit. + # +nil+ means no limit. def in_clause_length nil end @@ -61,7 +60,6 @@ module ActiveRecord def joins_per_query 256 end - end 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 38dd9578fe..2c352819fb 100644 --- a/activerecord/lib/active_record/connection_adapters/abstract/database_statements.rb +++ b/activerecord/lib/active_record/connection_adapters/abstract/database_statements.rb @@ -10,26 +10,37 @@ module ActiveRecord def to_sql(arel, binds = []) if arel.respond_to?(:ast) collected = visitor.accept(arel.ast, collector) - collected.compile(binds.dup, self) + collected.compile(binds, self).freeze else - arel + arel.dup.freeze end end # This is used in the StatementCache object. It returns an object that # can be used to query the database repeatedly. - def cacheable_query(arel) # :nodoc: + def cacheable_query(klass, arel) # :nodoc: + collected = visitor.accept(arel.ast, collector) if prepared_statements - ActiveRecord::StatementCache.query visitor, arel.ast + klass.query(collected.value) else - ActiveRecord::StatementCache.partial_query visitor, arel.ast, collector + klass.partial_query(collected.value) end end # Returns an ActiveRecord::Result instance. - def select_all(arel, name = nil, binds = []) + def select_all(arel, name = nil, binds = [], preparable: nil) arel, binds = binds_from_relation arel, binds - select(to_sql(arel, binds), name, binds) + sql = to_sql(arel, binds) + if !prepared_statements || (arel.is_a?(String) && preparable.nil?) + preparable = false + else + preparable = visitor.preparable + end + if prepared_statements && preparable + select_prepared(sql, name, binds) + else + select(sql, name, binds) + end end # Returns a record hash with the column names as keys and column values @@ -48,39 +59,45 @@ module ActiveRecord # Returns an array of the values of the first column in a select: # select_values("SELECT id FROM companies LIMIT 3") => [1,2,3] - def select_values(arel, name = nil) - arel, binds = binds_from_relation arel, [] + def select_values(arel, name = nil, binds = []) + arel, binds = binds_from_relation arel, binds select_rows(to_sql(arel, binds), name, binds).map(&:first) end # Returns an array of arrays containing the field values. # Order is the same as that returned by +columns+. def select_rows(sql, name = nil, binds = []) + exec_query(sql, name, binds).rows end - undef_method :select_rows - # Executes the SQL statement in the context of this connection. + # Executes the SQL statement in the context of this connection and returns + # the raw result from the connection adapter. + # Note: depending on your database connector, the result returned by this + # method may be manually memory managed. Consider using the exec_query + # wrapper instead. def execute(sql, name = nil) + raise NotImplementedError end - undef_method :execute # Executes +sql+ statement in the context of this connection using # +binds+ as the bind substitutes. +name+ is logged along with # the executed +sql+ statement. - def exec_query(sql, name = 'SQL', binds = []) + def exec_query(sql, name = "SQL", binds = [], prepare: false) + raise NotImplementedError end # Executes insert +sql+ statement in the context of this connection using # +binds+ as the bind substitutes. +name+ is logged along with # the executed +sql+ statement. - def exec_insert(sql, name, binds, pk = nil, sequence_name = nil) + def exec_insert(sql, name = nil, binds = [], pk = nil, sequence_name = nil) + sql, binds = sql_for_insert(sql, pk, nil, sequence_name, binds) exec_query(sql, name, binds) end # Executes delete +sql+ statement in the context of this connection using # +binds+ as the bind substitutes. +name+ is logged along with # the executed +sql+ statement. - def exec_delete(sql, name, binds) + def exec_delete(sql, name = nil, binds = []) exec_query(sql, name, binds) end @@ -92,23 +109,23 @@ module ActiveRecord # Executes update +sql+ statement in the context of this connection using # +binds+ as the bind substitutes. +name+ is logged along with # the executed +sql+ statement. - def exec_update(sql, name, binds) + def exec_update(sql, name = nil, binds = []) exec_query(sql, name, binds) end - # Returns the last auto-generated ID from the affected table. + # Executes an INSERT query and returns the new record's ID # - # +id_value+ will be returned unless the value is nil, in + # +id_value+ will be returned unless the value is +nil+, in # which case the database will attempt to calculate the last inserted # id and return that value. # # If the next id was calculated in advance (as in Oracle), it should be # passed in as +id_value+. def insert(arel, name = nil, pk = nil, id_value = nil, sequence_name = nil, binds = []) - sql, binds = sql_for_insert(to_sql(arel, binds), pk, id_value, sequence_name, binds) - value = exec_insert(sql, name, binds, pk, sequence_name) + value = exec_insert(to_sql(arel, binds), name, binds, pk, sequence_name) id_value || last_inserted_id(value) end + alias create insert # Executes the update statement and returns the number of rows affected. def update(arel, name = nil, binds = []) @@ -137,7 +154,7 @@ module ActiveRecord # # In order to get around this problem, #transaction will emulate the effect # of nested transactions, by using savepoints: - # http://dev.mysql.com/doc/refman/5.6/en/savepoint.html + # http://dev.mysql.com/doc/refman/5.7/en/savepoint.html # Savepoints are supported by MySQL and PostgreSQL. SQLite3 version >= '3.6.8' # supports savepoints. # @@ -190,18 +207,16 @@ module ActiveRecord # semantics of these different levels: # # * http://www.postgresql.org/docs/current/static/transaction-iso.html - # * https://dev.mysql.com/doc/refman/5.6/en/set-transaction.html + # * https://dev.mysql.com/doc/refman/5.7/en/set-transaction.html # - # An <tt>ActiveRecord::TransactionIsolationError</tt> will be raised if: + # An ActiveRecord::TransactionIsolationError will be raised if: # # * The adapter does not support setting the isolation level # * You are joining an existing open transaction # * 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. + # The mysql2 and postgresql adapters support setting the transaction + # isolation level. def transaction(requires_new: nil, isolation: nil, joinable: true) if !requires_new && current_transaction.joinable? if isolation @@ -224,7 +239,7 @@ module ActiveRecord end def reset_transaction #:nodoc: - @transaction_manager = TransactionManager.new(self) + @transaction_manager = ConnectionAdapters::TransactionManager.new(self) end # Register a record with the current transaction so that its after_commit and after_rollback callbacks @@ -271,9 +286,6 @@ module ActiveRecord exec_rollback_to_savepoint(name) end - def exec_rollback_to_savepoint(name = nil) #:nodoc: - end - def default_sequence_name(table, column) nil end @@ -286,14 +298,19 @@ module ActiveRecord # Inserts the given fixture into the table. Overridden in adapters that require # something beyond a simple insert (eg. Oracle). def insert_fixture(fixture, table_name) - columns = schema_cache.columns_hash(table_name) + fixture = fixture.stringify_keys + columns = schema_cache.columns_hash(table_name) binds = fixture.map do |name, value| - type = lookup_cast_type_from_column(columns[name]) - Relation::QueryAttribute.new(name, value, type) + if column = columns[name] + type = lookup_cast_type_from_column(column) + Relation::QueryAttribute.new(name, value, type) + else + raise Fixture::FixtureError, %(table "#{table_name}" has no column named #{name.inspect}.) + end end key_list = fixture.keys.map { |name| quote_column_name(name) } - value_list = prepare_binds_for_database(binds).map do |value| + value_list = binds.map(&:value_for_database).map do |value| begin quote(value) rescue TypeError @@ -301,7 +318,7 @@ module ActiveRecord end end - execute "INSERT INTO #{quote_table_name(table_name)} (#{key_list.join(', ')}) VALUES (#{value_list.join(', ')})", 'Fixture Insert' + execute "INSERT INTO #{quote_table_name(table_name)} (#{key_list.join(', ')}) VALUES (#{value_list.join(', ')})", "Fixture Insert" end def empty_insert_statement_value @@ -311,17 +328,12 @@ module ActiveRecord # Sanitizes the given LIMIT parameter in order to prevent SQL injection. # # The +limit+ may be anything that can evaluate to a string via #to_s. It - # should look like an integer, or a comma-delimited list of integers, or - # an Arel SQL literal. + # should look like an integer, or an Arel SQL literal. # # Returns Integer and Arel::Nodes::SqlLiteral limits as is. - # Returns the sanitized limit parameter, either as an integer, or as a - # string which contains a comma-delimited list of integers. def sanitize_limit(limit) if limit.is_a?(Integer) || limit.is_a?(Arel::Nodes::SqlLiteral) limit - elsif limit.to_s.include?(',') - Arel.sql limit.to_s.split(',').map{ |i| Integer(i) }.join(',') else Integer(limit) end @@ -330,20 +342,14 @@ module ActiveRecord # The default strategy for an UPDATE with joins is to use a subquery. This doesn't work # on MySQL (even when aliasing the tables), but MySQL allows using JOIN directly in # an UPDATE statement, so in the MySQL adapters we redefine this to do that. - def join_to_update(update, select) #:nodoc: - key = update.key + def join_to_update(update, select, key) # :nodoc: subselect = subquery_for(key, select) update.where key.in(subselect) end + alias join_to_delete join_to_update - def join_to_delete(delete, select, key) #:nodoc: - subselect = subquery_for(key, select) - - delete.where key.in(subselect) - end - - protected + private # Returns a subquery for the given key using the join information. def subquery_for(key, select) @@ -354,24 +360,11 @@ module ActiveRecord # Returns an ActiveRecord::Result instance. def select(sql, name = nil, binds = []) - exec_query(sql, name, binds) - end - - - # Returns the last auto-generated ID from the affected table. - def insert_sql(sql, name = nil, pk = nil, id_value = nil, sequence_name = nil) - execute(sql, name) - id_value - end - - # Executes the update statement and returns the number of rows affected. - def update_sql(sql, name = nil) - execute(sql, name) + exec_query(sql, name, binds, prepare: false) end - # Executes the delete statement and returns the number of rows affected. - def delete_sql(sql, name = nil) - update_sql(sql, name) + def select_prepared(sql, name = nil, binds = []) + exec_query(sql, name, binds, prepare: true) end def sql_for_insert(sql, pk, id_value, sequence_name, binds) diff --git a/activerecord/lib/active_record/connection_adapters/abstract/query_cache.rb b/activerecord/lib/active_record/connection_adapters/abstract/query_cache.rb index 5e27cfe507..7eab7de5d3 100644 --- a/activerecord/lib/active_record/connection_adapters/abstract/query_cache.rb +++ b/activerecord/lib/active_record/connection_adapters/abstract/query_cache.rb @@ -4,6 +4,9 @@ module ActiveRecord class << self def included(base) #:nodoc: dirties_query_cache base, :insert, :update, :delete, :rollback_to_savepoint, :rollback_db_transaction + + base.set_callback :checkout, :after, :configure_query_cache! + base.set_callback :checkin, :after, :disable_query_cache! end def dirties_query_cache(base, *method_names) @@ -18,11 +21,32 @@ module ActiveRecord end end + module ConnectionPoolConfiguration + def initialize(*) + super + @query_cache_enabled = Concurrent::Map.new { false } + end + + def enable_query_cache! + @query_cache_enabled[connection_cache_key(Thread.current)] = true + connection.enable_query_cache! if active_connection? + end + + def disable_query_cache! + @query_cache_enabled.delete connection_cache_key(Thread.current) + connection.disable_query_cache! if active_connection? + end + + def query_cache_enabled + @query_cache_enabled[connection_cache_key(Thread.current)] + end + end + attr_reader :query_cache, :query_cache_enabled def initialize(*) super - @query_cache = Hash.new { |h,sql| h[sql] = {} } + @query_cache = Hash.new { |h, sql| h[sql] = {} } @query_cache_enabled = false end @@ -41,6 +65,7 @@ module ActiveRecord def disable_query_cache! @query_cache_enabled = false + clear_query_cache end # Disable the query cache within the block. @@ -61,11 +86,11 @@ module ActiveRecord @query_cache.clear end - def select_all(arel, name = nil, binds = []) + def select_all(arel, name = nil, binds = [], preparable: nil) if @query_cache_enabled && !locked?(arel) arel, binds = binds_from_relation arel, binds sql = to_sql(arel, binds) - cache_sql(sql, binds) { super(sql, name, binds) } + cache_sql(sql, name, binds) { super(sql, name, binds, preparable: preparable) } else super end @@ -73,23 +98,33 @@ module ActiveRecord private - def cache_sql(sql, binds) - result = - if @query_cache[sql].key?(binds) - ActiveSupport::Notifications.instrument("sql.active_record", - :sql => sql, :binds => binds, :name => "CACHE", :connection_id => object_id) - @query_cache[sql][binds] - else - @query_cache[sql][binds] = yield - end - result.dup - end + def cache_sql(sql, name, binds) + result = + if @query_cache[sql].key?(binds) + ActiveSupport::Notifications.instrument( + "sql.active_record", + sql: sql, + binds: binds, + name: name, + connection_id: object_id, + cached: true, + ) + @query_cache[sql][binds] + else + @query_cache[sql][binds] = yield + end + result.dup + end - # If arel is locked this is a SELECT ... FOR UPDATE or somesuch. Such - # queries should not be cached. - def locked?(arel) - arel.respond_to?(:locked) && arel.locked - end + # If arel is locked this is a SELECT ... FOR UPDATE or somesuch. Such + # queries should not be cached. + def locked?(arel) + arel.respond_to?(:locked) && arel.locked + end + + def configure_query_cache! + enable_query_cache! if pool.query_cache_enabled + end end end end diff --git a/activerecord/lib/active_record/connection_adapters/abstract/quoting.rb b/activerecord/lib/active_record/connection_adapters/abstract/quoting.rb index 2c7409b2dc..0c6bc16e6f 100644 --- a/activerecord/lib/active_record/connection_adapters/abstract/quoting.rb +++ b/activerecord/lib/active_record/connection_adapters/abstract/quoting.rb @@ -1,24 +1,14 @@ -require 'active_support/core_ext/big_decimal/conversions' +require "active_support/core_ext/big_decimal/conversions" module ActiveRecord module ConnectionAdapters # :nodoc: module Quoting # Quotes the column value to help prevent # {SQL injection attacks}[http://en.wikipedia.org/wiki/SQL_injection]. - def quote(value, column = nil) + def quote(value) # records are quoted as their primary key return value.quoted_id if value.respond_to?(:quoted_id) - if column - ActiveSupport::Deprecation.warn(<<-MSG.squish) - Passing a column to `quote` has been deprecated. It is only used - for type casting, which should be handled elsewhere. See - https://github.com/rails/arel/commit/6160bfbda1d1781c3b08a33ec4955f170e95be11 - for more information. - MSG - value = type_cast_from_column(column, value) - end - _quote(value) end @@ -43,9 +33,9 @@ module ActiveRecord # If you are having to call this function, you are likely doing something # wrong. The column does not have sufficient type information if the user # provided a custom type on the class level either explicitly (via - # `attribute`) or implicitly (via `serialize`, - # `time_zone_aware_attributes`). In almost all cases, the sql type should - # only be used to change quoting behavior, when the primitive to + # Attributes::ClassMethods#attribute) or implicitly (via + # AttributeMethods::Serialization::ClassMethods#serialize, +time_zone_aware_attributes+). + # In almost all cases, the sql type should only be used to change quoting behavior, when the primitive to # represent the type doesn't sufficiently reflect the differences # (varchar vs binary) for example. The type used to get this primitive # should have been provided before reaching the connection adapter. @@ -58,7 +48,7 @@ module ActiveRecord end end - # See docs for +type_cast_from_column+ + # See docs for #type_cast_from_column def lookup_cast_type_from_column(column) # :nodoc: lookup_cast_type(column.sql_type) end @@ -82,7 +72,7 @@ module ActiveRecord # Quotes the column name. Defaults to no quoting. def quote_column_name(column_name) - column_name + column_name.to_s end # Quotes the table name. Defaults to column name quoting. @@ -93,7 +83,7 @@ module ActiveRecord # Override to return the quoted table name for assignment. Defaults to # table quoting. # - # This works for mysql and mysql2 where table.column can be used to + # This works for mysql2 where table.column can be used to # resolve ambiguity. # # We override this in the sqlite3 and postgresql adapters to use only @@ -102,25 +92,29 @@ module ActiveRecord quote_table_name("#{table}.#{attr}") end - def quote_default_expression(value, column) #:nodoc: - value = lookup_cast_type(column.sql_type).serialize(value) - quote(value) + def quote_default_expression(value, column) # :nodoc: + if value.is_a?(Proc) + value.call + else + value = lookup_cast_type(column.sql_type).serialize(value) + quote(value) + end end def quoted_true - "'t'" + "'t'".freeze end def unquoted_true - 't' + "t".freeze end def quoted_false - "'f'" + "'f'".freeze end def unquoted_false - 'f' + "f".freeze end # Quote date/time values for use in SQL input. Includes microseconds @@ -142,47 +136,53 @@ module ActiveRecord end end - def prepare_binds_for_database(binds) # :nodoc: - binds.map(&:value_for_database) + def quoted_time(value) # :nodoc: + quoted_date(value).sub(/\A2000-01-01 /, "") end private - def types_which_need_no_typecasting - [nil, Numeric, String] - end - - def _quote(value) - case value - when String, ActiveSupport::Multibyte::Chars, Type::Binary::Data - "'#{quote_string(value.to_s)}'" - when true then quoted_true - when false then quoted_false - when nil then "NULL" - # BigDecimals need to be put in a non-normalized form and quoted. - when BigDecimal then value.to_s('F') - when Numeric, ActiveSupport::Duration then value.to_s - when Date, Time then "'#{quoted_date(value)}'" - when Symbol then "'#{quote_string(value.to_s)}'" - when Class then "'#{value}'" - else raise TypeError, "can't quote #{value.class.name}" + def type_casted_binds(binds) + binds.map { |attr| type_cast(attr.value_for_database) } end - end - def _type_cast(value) - case value - when Symbol, ActiveSupport::Multibyte::Chars, Type::Binary::Data - value.to_s - when true then unquoted_true - when false then unquoted_false - # BigDecimals need to be put in a non-normalized form and quoted. - when BigDecimal then value.to_s('F') - when Date, Time then quoted_date(value) - when *types_which_need_no_typecasting - value - else raise TypeError + def types_which_need_no_typecasting + [nil, Numeric, String] + end + + def _quote(value) + case value + when String, ActiveSupport::Multibyte::Chars, Type::Binary::Data + "'#{quote_string(value.to_s)}'" + when true then quoted_true + when false then quoted_false + when nil then "NULL" + # BigDecimals need to be put in a non-normalized form and quoted. + when BigDecimal then value.to_s("F") + when Numeric, ActiveSupport::Duration then value.to_s + when Type::Time::Value then "'#{quoted_time(value)}'" + when Date, Time then "'#{quoted_date(value)}'" + when Symbol then "'#{quote_string(value.to_s)}'" + when Class then "'#{value}'" + else raise TypeError, "can't quote #{value.class.name}" + end + end + + def _type_cast(value) + case value + when Symbol, ActiveSupport::Multibyte::Chars, Type::Binary::Data + value.to_s + when true then unquoted_true + when false then unquoted_false + # BigDecimals need to be put in a non-normalized form and quoted. + when BigDecimal then value.to_s("F") + when Type::Time::Value then quoted_time(value) + when Date, Time then quoted_date(value) + when *types_which_need_no_typecasting + value + else raise TypeError + end end - end end end end diff --git a/activerecord/lib/active_record/connection_adapters/abstract/savepoints.rb b/activerecord/lib/active_record/connection_adapters/abstract/savepoints.rb index c0662f8473..3a06f75292 100644 --- a/activerecord/lib/active_record/connection_adapters/abstract/savepoints.rb +++ b/activerecord/lib/active_record/connection_adapters/abstract/savepoints.rb @@ -1,8 +1,8 @@ module ActiveRecord module ConnectionAdapters - module Savepoints #:nodoc: - def supports_savepoints? - true + module Savepoints + def current_savepoint_name + current_transaction.savepoint_name end def create_savepoint(name = current_savepoint_name) diff --git a/activerecord/lib/active_record/connection_adapters/abstract/schema_creation.rb b/activerecord/lib/active_record/connection_adapters/abstract/schema_creation.rb index 18d943f452..322684672f 100644 --- a/activerecord/lib/active_record/connection_adapters/abstract/schema_creation.rb +++ b/activerecord/lib/active_record/connection_adapters/abstract/schema_creation.rb @@ -1,4 +1,4 @@ -require 'active_support/core_ext/string/strip' +require "active_support/core_ext/string/strip" module ActiveRecord module ConnectionAdapters @@ -14,16 +14,18 @@ module ActiveRecord send m, o end - delegate :quote_column_name, :quote_table_name, :quote_default_expression, :type_to_sql, to: :@conn - private :quote_column_name, :quote_table_name, :quote_default_expression, :type_to_sql + delegate :quote_column_name, :quote_table_name, :quote_default_expression, :type_to_sql, + :options_include_default?, :supports_indexes_in_create?, :supports_foreign_keys?, :foreign_key_options, to: :@conn + private :quote_column_name, :quote_table_name, :quote_default_expression, :type_to_sql, + :options_include_default?, :supports_indexes_in_create?, :supports_foreign_keys?, :foreign_key_options private def visit_AlterTable(o) sql = "ALTER TABLE #{quote_table_name(o.name)} " - sql << o.adds.map { |col| accept col }.join(' ') - sql << o.foreign_key_adds.map { |fk| visit_AddForeignKey fk }.join(' ') - sql << o.foreign_key_drops.map { |fk| visit_DropForeignKey fk }.join(' ') + sql << o.adds.map { |col| accept col }.join(" ") + sql << o.foreign_key_adds.map { |fk| visit_AddForeignKey fk }.join(" ") + sql << o.foreign_key_drops.map { |fk| visit_DropForeignKey fk }.join(" ") end def visit_ColumnDefinition(o) @@ -38,17 +40,32 @@ module ActiveRecord end def visit_TableDefinition(o) - create_sql = "CREATE#{' TEMPORARY' if o.temporary} TABLE " - create_sql << "#{quote_table_name(o.name)} " - create_sql << "(#{o.columns.map { |c| accept c }.join(', ')}) " unless o.as - create_sql << "#{o.options}" + create_sql = "CREATE#{' TEMPORARY' if o.temporary} TABLE #{quote_table_name(o.name)} " + + statements = o.columns.map { |c| accept c } + statements << accept(o.primary_keys) if o.primary_keys + + if supports_indexes_in_create? + statements.concat(o.indexes.map { |column_name, options| index_in_create(o.name, column_name, options) }) + end + + if supports_foreign_keys? + statements.concat(o.foreign_keys.map { |to_table, options| foreign_key_in_create(o.name, to_table, options) }) + end + + create_sql << "(#{statements.join(', ')})" if statements.present? + add_table_options!(create_sql, table_options(o)) create_sql << " AS #{@conn.to_sql(o.as)}" if o.as create_sql end - def visit_AddForeignKey(o) + def visit_PrimaryKeyDefinition(o) + "PRIMARY KEY (#{o.name.join(', ')})" + end + + def visit_ForeignKeyDefinition(o) sql = <<-SQL.strip_heredoc - ADD CONSTRAINT #{quote_column_name(o.name)} + CONSTRAINT #{quote_column_name(o.name)} FOREIGN KEY (#{quote_column_name(o.column)}) REFERENCES #{quote_table_name(o.to_table)} (#{quote_column_name(o.primary_key)}) SQL @@ -57,10 +74,27 @@ module ActiveRecord sql end + def visit_AddForeignKey(o) + "ADD #{accept(o)}" + end + def visit_DropForeignKey(name) "DROP CONSTRAINT #{quote_column_name(name)}" end + def table_options(o) + table_options = {} + table_options[:comment] = o.comment + table_options[:options] = o.options + table_options + end + + def add_table_options!(create_sql, options) + if options_sql = options[:options] + create_sql << " #{options_sql}" + end + end + def column_options(o) column_options = {} column_options[:null] = o.null unless o.null.nil? @@ -71,6 +105,7 @@ module ActiveRecord column_options[:auto_increment] = o.auto_increment column_options[:primary_key] = o.primary_key column_options[:collation] = o.collation + column_options[:comment] = o.comment column_options end @@ -89,8 +124,9 @@ module ActiveRecord sql end - def options_include_default?(options) - options.include?(:default) && !(options[:null] == false && options[:default].nil?) + def foreign_key_in_create(from_table, to_table, options) + options = foreign_key_options(from_table, to_table, options) + accept ForeignKeyDefinition.new(from_table, to_table, options) end def action_sql(action, dependency) 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 d17e272ed1..9b324c090b 100644 --- a/activerecord/lib/active_record/connection_adapters/abstract/schema_definitions.rb +++ b/activerecord/lib/active_record/connection_adapters/abstract/schema_definitions.rb @@ -3,15 +3,14 @@ module ActiveRecord # Abstract representation of an index definition on a table. Instances of # this type are typically created and returned by methods in database # adapters. e.g. ActiveRecord::ConnectionAdapters::AbstractMysqlAdapter#indexes - class IndexDefinition < Struct.new(:table, :name, :unique, :columns, :lengths, :orders, :where, :type, :using) #:nodoc: + class IndexDefinition < Struct.new(:table, :name, :unique, :columns, :lengths, :orders, :where, :type, :using, :comment) #:nodoc: end # Abstract representation of a column definition. Instances of this type # are typically created by methods in TableDefinition, and added to the # +columns+ attribute of said TableDefinition object, in order to be used # for generating a number of table creation or table changing SQL statements. - class ColumnDefinition < Struct.new(:name, :type, :limit, :precision, :scale, :default, :null, :first, :after, :auto_increment, :primary_key, :collation, :sql_type) #:nodoc: - + class ColumnDefinition < Struct.new(:name, :type, :limit, :precision, :scale, :default, :null, :first, :after, :auto_increment, :primary_key, :collation, :sql_type, :comment) #:nodoc: def primary_key? primary_key || type.to_sym == :primary_key end @@ -23,6 +22,9 @@ module ActiveRecord class ChangeColumnDefinition < Struct.new(:column, :name) #:nodoc: end + class PrimaryKeyDefinition < Struct.new(:name) # :nodoc: + end + class ForeignKeyDefinition < Struct.new(:from_table, :to_table, :options) #:nodoc: def name options[:name] @@ -48,27 +50,28 @@ module ActiveRecord options[:primary_key] != default_primary_key end - def defined_for?(options_or_to_table = {}) - if options_or_to_table.is_a?(Hash) - options_or_to_table.all? {|key, value| options[key].to_s == value.to_s } + def defined_for?(to_table_ord = nil, to_table: nil, **options) + if to_table_ord + self.to_table == to_table_ord.to_s else - to_table == options_or_to_table.to_s + (to_table.nil? || to_table.to_s == self.to_table) && + options.all? { |k, v| self.options[k].to_s == v.to_s } end end private - def default_primary_key - "id" - end + def default_primary_key + "id" + end end class ReferenceDefinition # :nodoc: def initialize( name, polymorphic: false, - index: false, + index: true, foreign_key: false, - type: :integer, + type: :bigint, **options ) @name = name @@ -97,47 +100,55 @@ module ActiveRecord end end + # TODO Change this to private once we've dropped Ruby 2.2 support. + # Workaround for Ruby 2.2 "private attribute?" warning. protected - attr_reader :name, :polymorphic, :index, :foreign_key, :type, :options + attr_reader :name, :polymorphic, :index, :foreign_key, :type, :options private - def as_options(value, default = {}) - if value.is_a?(Hash) - value - else - default + def as_options(value, default = {}) + if value.is_a?(Hash) + value + else + default + end end - end - def polymorphic_options - as_options(polymorphic, options) - end + def polymorphic_options + as_options(polymorphic, options) + end - def index_options - as_options(index) - end + def index_options + as_options(index) + end - def foreign_key_options - as_options(foreign_key) - end + def foreign_key_options + as_options(foreign_key).merge(column: column_name) + end - def columns - result = [["#{name}_id", type, options]] - if polymorphic - result.unshift(["#{name}_type", :string, polymorphic_options]) + def columns + result = [[column_name, type, options]] + if polymorphic + result.unshift(["#{name}_type", :string, polymorphic_options]) + end + result end - result - end - def column_names - columns.map(&:first) - end + def column_name + "#{name}_id" + end - def foreign_table_name - Base.pluralize_table_names ? name.to_s.pluralize : name - end + def column_names + columns.map(&:first) + end + + def foreign_table_name + foreign_key_options.fetch(:to_table) do + Base.pluralize_table_names ? name.to_s.pluralize : name + end + end end module ColumnMethods @@ -173,15 +184,16 @@ module ActiveRecord end CODE end + alias_method :numeric, :decimal end # Represents the schema of an SQL table in an abstract way. This class # provides methods for manipulating the schema representation. # - # Inside migration files, the +t+ object in +create_table+ + # Inside migration files, the +t+ object in {create_table}[rdoc-ref:SchemaStatements#create_table] # is actually of this type: # - # class SomeMigration < ActiveRecord::Migration + # class SomeMigration < ActiveRecord::Migration[5.0] # def up # create_table :foo do |t| # puts t.class # => "ActiveRecord::ConnectionAdapters::TableDefinition" @@ -193,27 +205,30 @@ module ActiveRecord # end # end # - # The table definitions - # The Columns are stored as a ColumnDefinition in the +columns+ attribute. class TableDefinition include ColumnMethods - # An array of ColumnDefinition objects, representing the column changes - # that have been defined. attr_accessor :indexes - attr_reader :name, :temporary, :options, :as, :foreign_keys + attr_reader :name, :temporary, :options, :as, :foreign_keys, :comment - def initialize(types, name, temporary, options, as = nil) + def initialize(name, temporary = false, options = nil, as = nil, comment: nil) @columns_hash = {} - @indexes = {} - @foreign_keys = {} - @native = types + @indexes = [] + @foreign_keys = [] + @primary_keys = nil @temporary = temporary @options = options @as = as @name = name + @comment = comment end + def primary_keys(name = nil) # :nodoc: + @primary_keys = PrimaryKeyDefinition.new(name) if name + @primary_keys + end + + # Returns an array of ColumnDefinition objects for the columns of the table. def columns; @columns_hash.values; end # Returns a ColumnDefinition for the column with name +name+. @@ -222,90 +237,23 @@ module ActiveRecord end # Instantiates a new column for the table. - # The +type+ parameter is normally one of the migrations native types, - # which is one of the following: - # <tt>:primary_key</tt>, <tt>:string</tt>, <tt>:text</tt>, - # <tt>:integer</tt>, <tt>:bigint</tt>, <tt>:float</tt>, <tt>:decimal</tt>, - # <tt>:datetime</tt>, <tt>:time</tt>, <tt>:date</tt>, - # <tt>:binary</tt>, <tt>:boolean</tt>. - # - # You may use a type not in this list as long as it is supported by your - # database (for example, "polygon" in MySQL), but this will not be database - # agnostic and should usually be avoided. - # - # Available options are (none of these exists by default): - # * <tt>:limit</tt> - - # Requests a maximum column length. This is number of characters for <tt>:string</tt> and - # <tt>:text</tt> columns and number of bytes for <tt>:binary</tt> and <tt>:integer</tt> columns. - # * <tt>:default</tt> - - # The column's default value. Use nil for NULL. - # * <tt>:null</tt> - - # Allows or disallows +NULL+ values in the column. This option could - # have been named <tt>:null_allowed</tt>. - # * <tt>:precision</tt> - - # Specifies the precision for a <tt>:decimal</tt> column. - # * <tt>:scale</tt> - - # Specifies the scale for a <tt>:decimal</tt> column. + # See {connection.add_column}[rdoc-ref:ConnectionAdapters::SchemaStatements#add_column] + # for available options. + # + # Additional options are: # * <tt>:index</tt> - # Create an index for the column. Can be either <tt>true</tt> or an options hash. # - # Note: The precision is the total number of significant digits - # and the scale is the number of digits that can be stored following - # the decimal point. For example, the number 123.45 has a precision of 5 - # and a scale of 2. A decimal with a precision of 5 and a scale of 2 can - # range from -999.99 to 999.99. - # - # Please be aware of different RDBMS implementations behavior with - # <tt>:decimal</tt> columns: - # * The SQL standard says the default scale should be 0, <tt>:scale</tt> <= - # <tt>:precision</tt>, and makes no comments about the requirements of - # <tt>:precision</tt>. - # * MySQL: <tt>:precision</tt> [1..63], <tt>:scale</tt> [0..30]. - # Default is (10,0). - # * PostgreSQL: <tt>:precision</tt> [1..infinity], - # <tt>:scale</tt> [0..infinity]. No default. - # * SQLite2: Any <tt>:precision</tt> and <tt>:scale</tt> may be used. - # Internal storage as strings. No default. - # * SQLite3: No restrictions on <tt>:precision</tt> and <tt>:scale</tt>, - # but the maximum supported <tt>:precision</tt> is 16. No default. - # * Oracle: <tt>:precision</tt> [1..38], <tt>:scale</tt> [-84..127]. - # Default is (38,0). - # * DB2: <tt>:precision</tt> [1..63], <tt>:scale</tt> [0..62]. - # Default unknown. - # * SqlServer?: <tt>:precision</tt> [1..38], <tt>:scale</tt> [0..38]. - # Default (38,0). - # # This method returns <tt>self</tt>. # # == Examples - # # Assuming +td+ is an instance of TableDefinition - # td.column(:granted, :boolean) - # # granted BOOLEAN - # - # td.column(:picture, :binary, limit: 2.megabytes) - # # => picture BLOB(2097152) - # - # td.column(:sales_stage, :string, limit: 20, default: 'new', null: false) - # # => sales_stage VARCHAR(20) DEFAULT 'new' NOT NULL # - # td.column(:bill_gates_money, :decimal, precision: 15, scale: 2) - # # => bill_gates_money DECIMAL(15,2) - # - # td.column(:sensor_reading, :decimal, precision: 30, scale: 20) - # # => sensor_reading DECIMAL(30,20) - # - # # While <tt>:scale</tt> defaults to zero on most databases, it - # # probably wouldn't hurt to include it. - # td.column(:huge_integer, :decimal, precision: 30) - # # => huge_integer DECIMAL(30) - # - # # Defines a column with a database-specific type. - # td.column(:foo, 'polygon') - # # => foo polygon + # # Assuming +td+ is an instance of TableDefinition + # td.column(:granted, :boolean, index: true) # # == Short-hand examples # - # Instead of calling +column+ directly, you can also work with the short-hand definitions for the default types. + # Instead of calling #column directly, you can also work with the short-hand definitions for the default types. # They use the type as the method name instead of as a parameter and allow for multiple columns to be defined # in a single statement. # @@ -337,7 +285,8 @@ module ActiveRecord # TableDefinition#references will add an appropriately-named _id column, plus a corresponding _type # column if the <tt>:polymorphic</tt> option is supplied. If <tt>:polymorphic</tt> is a hash of # options, these will be used when creating the <tt>_type</tt> column. The <tt>:index</tt> option - # will also create an index, similar to calling <tt>add_index</tt>. So what can be written like this: + # will also create an index, similar to calling {add_index}[rdoc-ref:ConnectionAdapters::SchemaStatements#add_index]. + # So what can be written like this: # # create_table :taggings do |t| # t.integer :tag_id, :tagger_id, :taggable_id @@ -356,7 +305,7 @@ module ActiveRecord # end def column(name, type, options = {}) name = name.to_s - type = type.to_sym + type = type.to_sym if type options = options.dup if @columns_hash[name] && @columns_hash[name].primary_key? @@ -369,6 +318,8 @@ module ActiveRecord self end + # remove the column +name+ from the table. + # remove_column(:account_id) def remove_column(name) @columns_hash.delete name.to_s end @@ -378,20 +329,21 @@ module ActiveRecord # # index(:account_id, name: 'index_projects_on_account_id') def index(column_name, options = {}) - indexes[column_name] = options + indexes << [column_name, options] end def foreign_key(table_name, options = {}) # :nodoc: - foreign_keys[table_name] = options + table_name_prefix = ActiveRecord::Base.table_name_prefix + table_name_suffix = ActiveRecord::Base.table_name_suffix + table_name = "#{table_name_prefix}#{table_name}#{table_name_suffix}" + foreign_keys.push([table_name, options]) end # Appends <tt>:datetime</tt> columns <tt>:created_at</tt> and - # <tt>:updated_at</tt> to the table. See SchemaStatements#add_timestamps + # <tt>:updated_at</tt> to the table. See {connection.add_timestamps}[rdoc-ref:SchemaStatements#add_timestamps] # # t.timestamps null: false - def timestamps(*args) - options = args.extract_options! - + def timestamps(**options) options[:null] = false if options[:null].nil? column(:created_at, :datetime, options) @@ -403,7 +355,7 @@ module ActiveRecord # t.references(:user) # t.belongs_to(:supplier, foreign_key: true) # - # See SchemaStatements#add_reference for details of the options you can use. + # See {connection.add_reference}[rdoc-ref:SchemaStatements#add_reference] for details of the options you can use. def references(*args, **options) args.each do |col| ReferenceDefinition.new(col, **options).add_to(self) @@ -414,11 +366,8 @@ module ActiveRecord def new_column_definition(name, type, options) # :nodoc: type = aliased_types(type.to_s, type) column = create_column_definition name, type - limit = options.fetch(:limit) do - native[type][:limit] if native[type].is_a?(Hash) - end - column.limit = limit + column.limit = options[:limit] column.precision = options[:precision] column.scale = options[:scale] column.default = options[:default] @@ -428,21 +377,18 @@ module ActiveRecord column.auto_increment = options[:auto_increment] column.primary_key = type == :primary_key || options[:primary_key] column.collation = options[:collation] + column.comment = options[:comment] column end private - def create_column_definition(name, type) - ColumnDefinition.new name, type - end - - def native - @native - end + def create_column_definition(name, type) + ColumnDefinition.new name, type + end - def aliased_types(name, fallback) - 'timestamp' == name ? :datetime : fallback - end + def aliased_types(name, fallback) + "timestamp" == name ? :datetime : fallback + end end class AlterTable # :nodoc: @@ -475,7 +421,7 @@ module ActiveRecord end # Represents an SQL table in an abstract way for updating a table. - # Also see TableDefinition and SchemaStatements#create_table + # Also see TableDefinition and {connection.create_table}[rdoc-ref:SchemaStatements#create_table] # # Available transformations are: # @@ -496,6 +442,7 @@ module ActiveRecord # t.bigint # t.float # t.decimal + # t.numeric # t.datetime # t.timestamp # t.time @@ -530,9 +477,9 @@ module ActiveRecord # Checks to see if a column exists. # - # t.string(:name) unless t.column_exists?(:name, :string) + # t.string(:name) unless t.column_exists?(:name, :string) # - # See SchemaStatements#column_exists? + # See {connection.column_exists?}[rdoc-ref:SchemaStatements#column_exists?] def column_exists?(column_name, type = nil, options = {}) @base.column_exists?(name, column_name, type, options) end @@ -544,18 +491,18 @@ module ActiveRecord # t.index([:branch_id, :party_id], unique: true) # t.index([:branch_id, :party_id], unique: true, name: 'by_branch_party') # - # See SchemaStatements#add_index for details of the options you can use. + # See {connection.add_index}[rdoc-ref:SchemaStatements#add_index] for details of the options you can use. def index(column_name, options = {}) @base.add_index(name, column_name, options) end # Checks to see if an index exists. # - # unless t.index_exists?(:branch_id) - # t.index(:branch_id) - # end + # unless t.index_exists?(:branch_id) + # t.index(:branch_id) + # end # - # See SchemaStatements#index_exists? + # See {connection.index_exists?}[rdoc-ref:SchemaStatements#index_exists?] def index_exists?(column_name, options = {}) @base.index_exists?(name, column_name, options) end @@ -564,7 +511,7 @@ module ActiveRecord # # t.rename_index(:user_id, :account_id) # - # See SchemaStatements#rename_index + # See {connection.rename_index}[rdoc-ref:SchemaStatements#rename_index] def rename_index(index_name, new_index_name) @base.rename_index(name, index_name, new_index_name) end @@ -573,7 +520,7 @@ module ActiveRecord # # t.timestamps(null: false) # - # See SchemaStatements#add_timestamps + # See {connection.add_timestamps}[rdoc-ref:SchemaStatements#add_timestamps] def timestamps(options = {}) @base.add_timestamps(name, options) end @@ -594,7 +541,7 @@ module ActiveRecord # t.change_default(:authorized, 1) # t.change_default(:status, from: nil, to: "draft") # - # See SchemaStatements#change_column_default + # See {connection.change_column_default}[rdoc-ref:SchemaStatements#change_column_default] def change_default(column_name, default_or_changes) @base.change_column_default(name, column_name, default_or_changes) end @@ -604,7 +551,7 @@ module ActiveRecord # t.remove(:qualification) # t.remove(:qualification, :experience) # - # See SchemaStatements#remove_columns + # See {connection.remove_columns}[rdoc-ref:SchemaStatements#remove_columns] def remove(*column_names) @base.remove_columns(name, *column_names) end @@ -615,7 +562,7 @@ module ActiveRecord # t.remove_index(column: [:branch_id, :party_id]) # t.remove_index(name: :by_branch_party) # - # See SchemaStatements#remove_index + # See {connection.remove_index}[rdoc-ref:SchemaStatements#remove_index] def remove_index(options = {}) @base.remove_index(name, options) end @@ -624,7 +571,7 @@ module ActiveRecord # # t.remove_timestamps # - # See SchemaStatements#remove_timestamps + # See {connection.remove_timestamps}[rdoc-ref:SchemaStatements#remove_timestamps] def remove_timestamps(options = {}) @base.remove_timestamps(name, options) end @@ -633,7 +580,7 @@ module ActiveRecord # # t.rename(:description, :name) # - # See SchemaStatements#rename_column + # See {connection.rename_column}[rdoc-ref:SchemaStatements#rename_column] def rename(column_name, new_column_name) @base.rename_column(name, column_name, new_column_name) end @@ -643,7 +590,7 @@ module ActiveRecord # t.references(:user) # t.belongs_to(:supplier, foreign_key: true) # - # See SchemaStatements#add_reference for details of the options you can use. + # See {connection.add_reference}[rdoc-ref:SchemaStatements#add_reference] for details of the options you can use. def references(*args) options = args.extract_options! args.each do |ref_name| @@ -657,7 +604,7 @@ module ActiveRecord # t.remove_references(:user) # t.remove_belongs_to(:supplier, polymorphic: true) # - # See SchemaStatements#remove_reference + # See {connection.remove_reference}[rdoc-ref:SchemaStatements#remove_reference] def remove_references(*args) options = args.extract_options! args.each do |ref_name| @@ -670,7 +617,7 @@ module ActiveRecord # # t.foreign_key(:authors) # - # See SchemaStatements#add_foreign_key + # See {connection.add_foreign_key}[rdoc-ref:SchemaStatements#add_foreign_key] def foreign_key(*args) # :nodoc: @base.add_foreign_key(name, *args) end @@ -679,15 +626,10 @@ module ActiveRecord # # t.foreign_key(:authors) unless t.foreign_key_exists?(:authors) # - # See SchemaStatements#foreign_key_exists? + # See {connection.foreign_key_exists?}[rdoc-ref:SchemaStatements#foreign_key_exists?] def foreign_key_exists?(*args) # :nodoc: @base.foreign_key_exists?(name, *args) end - - private - def native - @base.native_database_types - end end end end diff --git a/activerecord/lib/active_record/connection_adapters/abstract/schema_dumper.rb b/activerecord/lib/active_record/connection_adapters/abstract/schema_dumper.rb index b944a8631c..b912d24626 100644 --- a/activerecord/lib/active_record/connection_adapters/abstract/schema_dumper.rb +++ b/activerecord/lib/active_record/connection_adapters/abstract/schema_dumper.rb @@ -7,25 +7,20 @@ module ActiveRecord # Adapter level by over-writing this code inside the database specific adapters module ColumnDumper def column_spec(column) - spec = prepare_column_options(column) - (spec.keys - [:name, :type]).each{ |k| spec[k].insert(0, "#{k}: ")} - spec + [schema_type(column), prepare_column_options(column)] end def column_spec_for_primary_key(column) - return if column.type == :integer - spec = { id: column.type.inspect } - spec.merge!(prepare_column_options(column).delete_if { |key, _| [:name, :type].include?(key) }) + return {} if default_primary_key?(column) + spec = { id: schema_type(column).inspect } + spec.merge!(prepare_column_options(column).except!(:null)) end - # This can be overridden on a Adapter level basis to support other + # This can be overridden on an Adapter level basis to support other # extended datatypes (Example: Adding an array option in the - # PostgreSQLAdapter) + # PostgreSQL::ColumnDumper) def prepare_column_options(column) spec = {} - spec[:name] = column.name.inspect - spec[:type] = schema_type(column) - spec[:null] = 'false' unless column.null if limit = schema_limit(column) spec[:limit] = limit @@ -40,50 +35,68 @@ module ActiveRecord end default = schema_default(column) if column.has_default? - spec[:default] = default unless default.nil? + spec[:default] = default unless default.nil? + + spec[:null] = "false" unless column.null if collation = schema_collation(column) spec[:collation] = collation end + spec[:comment] = column.comment.inspect if column.comment.present? + spec end # Lists the valid migration options def migration_keys - [:name, :limit, :precision, :scale, :default, :null, :collation] + [:limit, :precision, :scale, :default, :null, :collation, :comment] end private - def schema_type(column) - column.type.to_s - end + def default_primary_key?(column) + schema_type(column) == :bigint + end - def schema_limit(column) - limit = column.limit || native_database_types[column.type][:limit] - limit.inspect if limit - end + def schema_type(column) + if column.bigint? + :bigint + else + column.type + end + end - def schema_precision(column) - column.precision.inspect if column.precision - end + def schema_limit(column) + limit = column.limit unless column.bigint? + limit.inspect if limit && limit != native_database_types[column.type][:limit] + end - def schema_scale(column) - column.scale.inspect if column.scale - end + def schema_precision(column) + column.precision.inspect if column.precision + end - def schema_default(column) - type = lookup_cast_type_from_column(column) - default = type.deserialize(column.default) - unless default.nil? - type.type_cast_for_schema(default) + def schema_scale(column) + column.scale.inspect if column.scale end - end - def schema_collation(column) - column.collation.inspect if column.collation - end + def schema_default(column) + type = lookup_cast_type_from_column(column) + default = type.deserialize(column.default) + if default.nil? + schema_expression(column) + else + type.type_cast_for_schema(default) + end + end + + def schema_expression(column) + "-> { #{column.default_function.inspect} }" if column.default_function + end + + def schema_collation(column) + column.collation.inspect if column.collation + end end end end diff --git a/activerecord/lib/active_record/connection_adapters/abstract/schema_statements.rb b/activerecord/lib/active_record/connection_adapters/abstract/schema_statements.rb index e3115abe66..2e5af1777c 100644 --- a/activerecord/lib/active_record/connection_adapters/abstract/schema_statements.rb +++ b/activerecord/lib/active_record/connection_adapters/abstract/schema_statements.rb @@ -1,6 +1,6 @@ -require 'active_record/migration/join_table' -require 'active_support/core_ext/string/access' -require 'digest' +require "active_record/migration/join_table" +require "active_support/core_ext/string/access" +require "digest" module ActiveRecord module ConnectionAdapters # :nodoc: @@ -18,9 +18,33 @@ module ActiveRecord nil end + # Returns the table comment that's stored in database metadata. + def table_comment(table_name) + nil + end + # Truncates a table alias according to the limits of the current adapter. def table_alias_for(table_name) - table_name[0...table_alias_length].tr('.', '_') + table_name[0...table_alias_length].tr(".", "_") + end + + # Returns the relation names useable to back Active Record models. + # For most adapters this means all #tables and #views. + def data_sources + tables | views + end + + # Checks to see if the data source +name+ exists on the database. + # + # data_source_exists?(:ebooks) + # + def data_source_exists?(name) + data_sources.include?(name.to_s) + end + + # Returns an array of table names defined in the database. + def tables + raise NotImplementedError, "#tables is not implemented" end # Checks to see if the table +table_name+ exists on the database. @@ -31,6 +55,19 @@ module ActiveRecord tables.include?(table_name.to_s) end + # Returns an array of view names defined in the database. + def views + raise NotImplementedError, "#views is not implemented" + end + + # Checks to see if the view +view_name+ exists on the database. + # + # view_exists?(:ebooks) + # + def view_exists?(view_name) + views.include?(view_name.to_s) + end + # Returns an array of indexes for the given table. # def indexes(table_name, name = nil) end @@ -50,18 +87,19 @@ module ActiveRecord # def index_exists?(table_name, column_name, options = {}) column_names = Array(column_name).map(&:to_s) - index_name = options.key?(:name) ? options[:name].to_s : index_name(table_name, column: column_names) checks = [] - checks << lambda { |i| i.name == index_name } checks << lambda { |i| i.columns == column_names } checks << lambda { |i| i.unique } if options[:unique] + checks << lambda { |i| i.name == options[:name].to_s } if options[:name] indexes(table_name).any? { |i| checks.all? { |check| check[i] } } end # Returns an array of Column objects for the table specified by +table_name+. # See the concrete implementation for details on the expected parameter values. - def columns(table_name) end + def columns(table_name) + raise NotImplementedError, "#columns is not implemented" + end # Checks to see if a column exists in a given table. # @@ -79,19 +117,27 @@ module ActiveRecord # def column_exists?(table_name, column_name, type = nil, options = {}) column_name = column_name.to_s - columns(table_name).any?{ |c| c.name == column_name && - (!type || c.type == type) && - (!options.key?(:limit) || c.limit == options[:limit]) && - (!options.key?(:precision) || c.precision == options[:precision]) && - (!options.key?(:scale) || c.scale == options[:scale]) && - (!options.key?(:default) || c.default == options[:default]) && - (!options.key?(:null) || c.null == options[:null]) } + checks = [] + checks << lambda { |c| c.name == column_name } + checks << lambda { |c| c.type == type } if type + migration_keys.each do |attr| + checks << lambda { |c| c.send(attr) == options[attr] } if options.key?(attr) + end + + columns(table_name).any? { |c| checks.all? { |check| check[c] } } + end + + # Returns just a table's primary key + def primary_key(table_name) + pk = primary_keys(table_name) + pk = pk.first unless pk.size > 1 + pk end # Creates a new table with the name +table_name+. +table_name+ may either # be a String or a Symbol. # - # There are two ways to work with +create_table+. You can use the block + # There are two ways to work with #create_table. You can use the block # form or the regular form, like this: # # === Block form @@ -123,15 +169,16 @@ module ActiveRecord # The +options+ hash can include the following keys: # [<tt>:id</tt>] # Whether to automatically add a primary key column. Defaults to true. - # Join tables for +has_and_belongs_to_many+ should set it to false. + # Join tables for {ActiveRecord::Base.has_and_belongs_to_many}[rdoc-ref:Associations::ClassMethods#has_and_belongs_to_many] should set it to false. # # A Symbol can be used to specify the type of the generated primary key column. # [<tt>:primary_key</tt>] # The name of the primary key, if one is to be added automatically. - # Defaults to +id+. If <tt>:id</tt> is false this option is ignored. + # Defaults to +id+. If <tt>:id</tt> is false, then this option is ignored. # # Note that Active Record models will automatically detect their - # primary key. This can be avoided by using +self.primary_key=+ on the model + # primary key. This can be avoided by using + # {self.primary_key=}[rdoc-ref:AttributeMethods::PrimaryKey::ClassMethods#primary_key=] on the model # to define the key explicitly. # # [<tt>:options</tt>] @@ -153,7 +200,7 @@ module ActiveRecord # generates: # # CREATE TABLE suppliers ( - # id int(11) DEFAULT NULL auto_increment PRIMARY KEY + # id int auto_increment PRIMARY KEY # ) ENGINE=InnoDB DEFAULT CHARSET=utf8 # # ====== Rename the primary key column @@ -165,7 +212,7 @@ module ActiveRecord # generates: # # CREATE TABLE objects ( - # guid int(11) DEFAULT NULL auto_increment PRIMARY KEY, + # guid int auto_increment PRIMARY KEY, # name varchar(80) # ) # @@ -207,33 +254,41 @@ module ActiveRecord # SELECT * FROM orders INNER JOIN line_items ON order_id=orders.id # # See also TableDefinition#column for details on how to create columns. - def create_table(table_name, options = {}) - td = create_table_definition table_name, options[:temporary], options[:options], options[:as] + def create_table(table_name, comment: nil, **options) + td = create_table_definition table_name, options[:temporary], options[:options], options[:as], comment: comment if options[:id] != false && !options[:as] pk = options.fetch(:primary_key) do Base.get_primary_key table_name.to_s.singularize end - td.primary_key pk, options.fetch(:id, :primary_key), options + if pk.is_a?(Array) + td.primary_keys pk + else + td.primary_key pk, options.fetch(:id, :primary_key), options + end end yield td if block_given? - if options[:force] && table_exists?(table_name) + if options[:force] && data_source_exists?(table_name) drop_table(table_name, options) end result = execute schema_creation.accept td unless supports_indexes_in_create? - td.indexes.each_pair do |column_name, index_options| + td.indexes.each do |column_name, index_options| add_index(table_name, column_name, index_options) end end - td.foreign_keys.each_pair do |other_table_name, foreign_key_options| - add_foreign_key(table_name, other_table_name, foreign_key_options) + if supports_comments? && !supports_comments_in_create? + change_table_comment(table_name, comment) if comment.present? + + td.columns.each do |column| + change_column_comment(table_name, column.name, column.comment) if column.comment.present? + end end result @@ -245,9 +300,9 @@ module ActiveRecord # # Creates a table called 'assemblies_parts' with no id. # create_join_table(:assemblies, :parts) # - # You can pass a +options+ hash can include the following keys: + # You can pass an +options+ hash which can include the following keys: # [<tt>:table_name</tt>] - # Sets the table name overriding the default + # Sets the table name, overriding the default. # [<tt>:column_options</tt>] # Any extra options you want appended to the columns definition. # [<tt>:options</tt>] @@ -258,7 +313,7 @@ module ActiveRecord # Set to true to drop the table before creating it. # Defaults to false. # - # Note that +create_join_table+ does not create any indices by default; you can use + # Note that #create_join_table does not create any indices by default; you can use # its block form to do so yourself: # # create_join_table :products, :categories do |t| @@ -282,22 +337,23 @@ module ActiveRecord column_options = options.delete(:column_options) || {} column_options.reverse_merge!(null: false) + type = column_options.delete(:type) || :integer - t1_column, t2_column = [table_1, table_2].map{ |t| t.to_s.singularize.foreign_key } + t1_column, t2_column = [table_1, table_2].map { |t| t.to_s.singularize.foreign_key } create_table(join_table_name, options.merge!(id: false)) do |td| - td.integer t1_column, column_options - td.integer t2_column, column_options + td.send type, t1_column, column_options + td.send type, t2_column, column_options yield td if block_given? end end # Drops the join table specified by the given arguments. - # See +create_join_table+ for details. + # See #create_join_table for details. # # Although this command ignores the block if one is given, it can be helpful # to provide one in a migration's +change+ method so it can be reverted. - # In that case, the block will be used by create_join_table. + # In that case, the block will be used by #create_join_table. def drop_join_table(table_1, table_2, options = {}) join_table_name = find_join_table_name(table_1, table_2, options) drop_table(join_table_name) @@ -315,7 +371,7 @@ module ActiveRecord # [<tt>:bulk</tt>] # Set this to true to make this a bulk alter query, such as # - # ALTER TABLE `users` ADD COLUMN age INT(11), ADD COLUMN birthdate DATETIME ... + # ALTER TABLE `users` ADD COLUMN age INT, ADD COLUMN birthdate DATETIME ... # # Defaults to false. # @@ -372,7 +428,7 @@ module ActiveRecord # t.remove_index :company_id # end # - # See also Table for details on all of the various column transformation. + # See also Table for details on all of the various column transformations. def change_table(table_name, options = {}) if supports_bulk_alter? && options[:bulk] recorder = ActiveRecord::Migration::CommandRecorder.new(self) @@ -402,17 +458,88 @@ module ActiveRecord # # Although this command ignores most +options+ and the block if one is given, # it can be helpful to provide these in a migration's +change+ method so it can be reverted. - # In that case, +options+ and the block will be used by create_table. + # In that case, +options+ and the block will be used by #create_table. def drop_table(table_name, options = {}) execute "DROP TABLE#{' IF EXISTS' if options[:if_exists]} #{quote_table_name(table_name)}" end - # Adds a new column to the named table. - # See TableDefinition#column for details of the options you can use. - # - # Note: Not all options will be available, generally this command should - # ignore most of them. In favor of doing a low-level call to simply - # create a column. + # Add a new +type+ column named +column_name+ to +table_name+. + # + # The +type+ parameter is normally one of the migrations native types, + # which is one of the following: + # <tt>:primary_key</tt>, <tt>:string</tt>, <tt>:text</tt>, + # <tt>:integer</tt>, <tt>:bigint</tt>, <tt>:float</tt>, <tt>:decimal</tt>, <tt>:numeric</tt>, + # <tt>:datetime</tt>, <tt>:time</tt>, <tt>:date</tt>, + # <tt>:binary</tt>, <tt>:boolean</tt>. + # + # You may use a type not in this list as long as it is supported by your + # database (for example, "polygon" in MySQL), but this will not be database + # agnostic and should usually be avoided. + # + # Available options are (none of these exists by default): + # * <tt>:limit</tt> - + # Requests a maximum column length. This is the number of characters for a <tt>:string</tt> column + # and number of bytes for <tt>:text</tt>, <tt>:binary</tt> and <tt>:integer</tt> columns. + # * <tt>:default</tt> - + # The column's default value. Use +nil+ for +NULL+. + # * <tt>:null</tt> - + # Allows or disallows +NULL+ values in the column. This option could + # have been named <tt>:null_allowed</tt>. + # * <tt>:precision</tt> - + # Specifies the precision for the <tt>:decimal</tt> and <tt>:numeric</tt> columns. + # * <tt>:scale</tt> - + # Specifies the scale for the <tt>:decimal</tt> and <tt>:numeric</tt> columns. + # + # Note: The precision is the total number of significant digits, + # and the scale is the number of digits that can be stored following + # the decimal point. For example, the number 123.45 has a precision of 5 + # and a scale of 2. A decimal with a precision of 5 and a scale of 2 can + # range from -999.99 to 999.99. + # + # Please be aware of different RDBMS implementations behavior with + # <tt>:decimal</tt> columns: + # * The SQL standard says the default scale should be 0, <tt>:scale</tt> <= + # <tt>:precision</tt>, and makes no comments about the requirements of + # <tt>:precision</tt>. + # * MySQL: <tt>:precision</tt> [1..63], <tt>:scale</tt> [0..30]. + # Default is (10,0). + # * PostgreSQL: <tt>:precision</tt> [1..infinity], + # <tt>:scale</tt> [0..infinity]. No default. + # * SQLite3: No restrictions on <tt>:precision</tt> and <tt>:scale</tt>, + # but the maximum supported <tt>:precision</tt> is 16. No default. + # * Oracle: <tt>:precision</tt> [1..38], <tt>:scale</tt> [-84..127]. + # Default is (38,0). + # * DB2: <tt>:precision</tt> [1..63], <tt>:scale</tt> [0..62]. + # Default unknown. + # * SqlServer: <tt>:precision</tt> [1..38], <tt>:scale</tt> [0..38]. + # Default (38,0). + # + # == Examples + # + # add_column(:users, :picture, :binary, limit: 2.megabytes) + # # ALTER TABLE "users" ADD "picture" blob(2097152) + # + # add_column(:articles, :status, :string, limit: 20, default: 'draft', null: false) + # # ALTER TABLE "articles" ADD "status" varchar(20) DEFAULT 'draft' NOT NULL + # + # add_column(:answers, :bill_gates_money, :decimal, precision: 15, scale: 2) + # # ALTER TABLE "answers" ADD "bill_gates_money" decimal(15,2) + # + # add_column(:measurements, :sensor_reading, :decimal, precision: 30, scale: 20) + # # ALTER TABLE "measurements" ADD "sensor_reading" decimal(30,20) + # + # # While :scale defaults to zero on most databases, it + # # probably wouldn't hurt to include it. + # add_column(:measurements, :huge_integer, :decimal, precision: 30) + # # ALTER TABLE "measurements" ADD "huge_integer" decimal(30) + # + # # Defines a column that stores an array of a type. + # add_column(:users, :skills, :text, array: true) + # # ALTER TABLE "users" ADD "skills" text[] + # + # # Defines a column with a database-specific type. + # add_column(:shapes, :triangle, 'polygon') + # # ALTER TABLE "shapes" ADD "triangle" polygon def add_column(table_name, column_name, type, options = {}) at = create_alter_table table_name at.add_column(column_name, type, options) @@ -436,7 +563,7 @@ module ActiveRecord # # The +type+ and +options+ parameters will be ignored if present. It can be helpful # to provide these in a migration's +change+ method so it can be reverted. - # In that case, +type+ and +options+ will be used by add_column. + # In that case, +type+ and +options+ will be used by #add_column. def remove_column(table_name, column_name, type = nil, options = {}) execute "ALTER TABLE #{quote_table_name(table_name)} DROP #{quote_column_name(column_name)}" end @@ -469,7 +596,7 @@ module ActiveRecord raise NotImplementedError, "change_column_default is not implemented" end - # Sets or removes a +NOT NULL+ constraint on a column. The +null+ flag + # Sets or removes a <tt>NOT NULL</tt> constraint on a column. The +null+ flag # indicates whether the value can be +NULL+. For example # # change_column_null(:users, :nickname, false) @@ -481,7 +608,7 @@ module ActiveRecord # allows them to be +NULL+ (drops the constraint). # # The method accepts an optional fourth argument to replace existing - # +NULL+s with some other value. Use that one when enabling the + # <tt>NULL</tt>s with some other value. Use that one when enabling the # constraint if needed, since otherwise those rows would not be valid. # # Please note the fourth argument does not set a column's default. @@ -535,6 +662,8 @@ module ActiveRecord # # CREATE INDEX by_name ON accounts(name(10)) # + # ====== Creating an index with specific key lengths for multiple keys + # # add_index(:accounts, [:name, :surname], name: 'by_name_surname', length: {name: 10, surname: 15}) # # generates: @@ -582,7 +711,7 @@ module ActiveRecord # # CREATE FULLTEXT INDEX index_developers_on_name ON developers (name) -- MySQL # - # Note: only supported by MySQL. Supported: <tt>:fulltext</tt> and <tt>:spatial</tt> on MyISAM tables. + # Note: only supported by MySQL. def add_index(table_name, column_name, options = {}) index_name, index_type, index_columns, index_options = add_index_options(table_name, column_name, options) execute "CREATE #{index_type} INDEX #{quote_column_name(index_name)} ON #{quote_table_name(table_name)} (#{index_columns})#{index_options}" @@ -590,15 +719,15 @@ module ActiveRecord # Removes the given index from the table. # - # Removes the +index_accounts_on_column+ in the +accounts+ table. + # Removes the index on +branch_id+ in the +accounts+ table if exactly one such index exists. # # remove_index :accounts, :branch_id # - # Removes the index named +index_accounts_on_branch_id+ in the +accounts+ table. + # Removes the index on +branch_id+ in the +accounts+ table if exactly one such index exists. # # remove_index :accounts, column: :branch_id # - # Removes the index named +index_accounts_on_branch_id_and_party_id+ in the +accounts+ table. + # Removes the index on +branch_id+ and +party_id+ in the +accounts+ table if exactly one such index exists. # # remove_index :accounts, column: [:branch_id, :party_id] # @@ -607,10 +736,7 @@ module ActiveRecord # 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 - - def remove_index!(table_name, index_name) #:nodoc: + index_name = index_name_for_remove(table_name, options) execute "DROP INDEX #{quote_column_name(index_name)} ON #{quote_table_name(table_name)}" end @@ -640,7 +766,7 @@ module ActiveRecord raise ArgumentError, "You must specify the index name" end else - index_name(table_name, :column => options) + index_name(table_name, column: options) end end @@ -657,17 +783,20 @@ module ActiveRecord # Adds a reference. The reference column is an integer by default, # the <tt>:type</tt> option can be used to specify a different type. # Optionally adds a +_type+ column, if <tt>:polymorphic</tt> option is provided. - # <tt>add_reference</tt> and <tt>add_belongs_to</tt> are acceptable. + # #add_reference and #add_belongs_to are acceptable. # # The +options+ hash can include the following keys: # [<tt>:type</tt>] # The reference column type. Defaults to +:integer+. # [<tt>:index</tt>] - # Add an appropriate index. Defaults to false. + # Add an appropriate index. Defaults to true. + # See #add_index for usage of this option. # [<tt>:foreign_key</tt>] - # Add an appropriate foreign key. Defaults to false. + # Add an appropriate foreign key constraint. Defaults to false. # [<tt>:polymorphic</tt>] # Whether an additional +_type+ column should be added. Defaults to false. + # [<tt>:null</tt>] + # Whether the column allows nulls. Defaults to true. # # ====== Create a user_id integer column # @@ -681,17 +810,29 @@ module ActiveRecord # # add_reference(:products, :supplier, polymorphic: true, index: true) # + # ====== Create a supplier_id column with a unique index + # + # add_reference(:products, :supplier, index: { unique: true }) + # + # ====== Create a supplier_id column with a named index + # + # add_reference(:products, :supplier, index: { name: "my_supplier_index" }) + # # ====== Create a supplier_id column and appropriate foreign key # # add_reference(:products, :supplier, foreign_key: true) # + # ====== Create a supplier_id column and a foreign key to the firms table + # + # add_reference(:products, :supplier, foreign_key: {to_table: :firms}) + # def add_reference(table_name, *args) ReferenceDefinition.new(*args).add_to(update_table_definition(table_name, self)) end alias :add_belongs_to :add_reference # Removes the reference(s). Also removes a +type+ column if one exists. - # <tt>remove_reference</tt> and <tt>remove_belongs_to</tt> are acceptable. + # #remove_reference and #remove_belongs_to are acceptable. # # ====== Remove the reference # @@ -705,19 +846,24 @@ module ActiveRecord # # remove_reference(:products, :user, index: true, foreign_key: true) # - def remove_reference(table_name, ref_name, options = {}) - if options[:foreign_key] + def remove_reference(table_name, ref_name, foreign_key: false, polymorphic: false, **options) + if foreign_key reference_name = Base.pluralize_table_names ? ref_name.to_s.pluralize : ref_name - remove_foreign_key(table_name, reference_name) + if foreign_key.is_a?(Hash) + foreign_key_options = foreign_key + else + foreign_key_options = { to_table: reference_name } + end + remove_foreign_key(table_name, **foreign_key_options) end remove_column(table_name, "#{ref_name}_id") - remove_column(table_name, "#{ref_name}_type") if options[:polymorphic] + remove_column(table_name, "#{ref_name}_type") if polymorphic end alias :remove_belongs_to :remove_reference # Returns an array of foreign keys for the given table. - # The foreign keys are represented as +ForeignKeyDefinition+ objects. + # The foreign keys are represented as ForeignKeyDefinition objects. def foreign_keys(table_name) raise NotImplementedError, "foreign_keys is not implemented" end @@ -735,7 +881,7 @@ module ActiveRecord # # generates: # - # ALTER TABLE "articles" ADD CONSTRAINT articles_author_id_fk FOREIGN KEY ("author_id") REFERENCES "authors" ("id") + # ALTER TABLE "articles" ADD CONSTRAINT fk_rails_e74ce85cbc FOREIGN KEY ("author_id") REFERENCES "authors" ("id") # # ====== Creating a foreign key on a specific column # @@ -751,7 +897,7 @@ module ActiveRecord # # generates: # - # ALTER TABLE "articles" ADD CONSTRAINT articles_author_id_fk FOREIGN KEY ("author_id") REFERENCES "authors" ("id") ON DELETE CASCADE + # ALTER TABLE "articles" ADD CONSTRAINT fk_rails_e74ce85cbc FOREIGN KEY ("author_id") REFERENCES "authors" ("id") ON DELETE CASCADE # # The +options+ hash can include the following keys: # [<tt>:column</tt>] @@ -761,21 +907,13 @@ module ActiveRecord # [<tt>:name</tt>] # The constraint name. Defaults to <tt>fk_rails_<identifier></tt>. # [<tt>:on_delete</tt>] - # Action that happens <tt>ON DELETE</tt>. Valid values are +:nullify+, +:cascade:+ and +:restrict+ + # Action that happens <tt>ON DELETE</tt>. Valid values are +:nullify+, +:cascade+ and +:restrict+ # [<tt>:on_update</tt>] - # Action that happens <tt>ON UPDATE</tt>. Valid values are +:nullify+, +:cascade:+ and +:restrict+ + # Action that happens <tt>ON UPDATE</tt>. Valid values are +:nullify+, +:cascade+ and +:restrict+ def add_foreign_key(from_table, to_table, options = {}) return unless supports_foreign_keys? - options[:column] ||= foreign_key_column_for(to_table) - - options = { - column: options[:column], - primary_key: options[:primary_key], - name: foreign_key_name(from_table, options), - on_delete: options[:on_delete], - on_update: options[:on_update] - } + options = foreign_key_options(from_table, to_table, options) at = create_alter_table from_table at.add_foreign_key to_table, options @@ -813,13 +951,13 @@ module ActiveRecord # Checks to see if a foreign key exists on a table for a given foreign key definition. # - # # Check a foreign key exists + # # Checks to see if a foreign key exists. # foreign_key_exists?(:accounts, :branches) # - # # Check a foreign key on a specified column exists + # # Checks to see if a foreign key on a specified column exists. # foreign_key_exists?(:accounts, column: :owner_id) # - # # Check a foreign key with a custom name exists + # # Checks to see if a foreign key with a custom name exists. # foreign_key_exists?(:accounts, name: "special_fk_name") # def foreign_key_exists?(from_table, options_or_to_table = {}) @@ -828,12 +966,12 @@ module ActiveRecord def foreign_key_for(from_table, options_or_to_table = {}) # :nodoc: return unless supports_foreign_keys? - foreign_keys(from_table).detect {|fk| fk.defined_for? options_or_to_table } + foreign_keys(from_table).detect { |fk| fk.defined_for? options_or_to_table } end def foreign_key_for!(from_table, options_or_to_table = {}) # :nodoc: - foreign_key_for(from_table, options_or_to_table) or \ - raise ArgumentError, "Table '#{from_table}' has no foreign key for #{options_or_to_table}" + foreign_key_for(from_table, options_or_to_table) || \ + raise(ArgumentError, "Table '#{from_table}' has no foreign key for #{options_or_to_table}") end def foreign_key_column_for(table_name) # :nodoc: @@ -843,12 +981,29 @@ module ActiveRecord "#{name.singularize}_id" end + def foreign_key_options(from_table, to_table, options) # :nodoc: + options = options.dup + options[:column] ||= foreign_key_column_for(to_table) + options[:name] ||= foreign_key_name(from_table, options) + options + end + def dump_schema_information #:nodoc: + versions = ActiveRecord::SchemaMigration.order("version").pluck(:version) + insert_versions_sql(versions) + end + + def insert_versions_sql(versions) # :nodoc: sm_table = ActiveRecord::Migrator.schema_migrations_table_name - ActiveRecord::SchemaMigration.order('version').map { |sm| - "INSERT INTO #{sm_table} (version) VALUES ('#{sm.version}');" - }.join "\n\n" + if versions.is_a?(Array) + sql = "INSERT INTO #{sm_table} (version) VALUES\n" + sql << versions.map { |v| "('#{v}')" }.join(",\n") + sql << ";\n\n" + sql + else + "INSERT INTO #{sm_table} (version) VALUES ('#{versions}');" + end end # Should not be called normally, but this operation is non-destructive. @@ -857,34 +1012,47 @@ module ActiveRecord ActiveRecord::SchemaMigration.create_table end - def assume_migrated_upto_version(version, migrations_paths = ActiveRecord::Migrator.migrations_paths) + def initialize_internal_metadata_table + ActiveRecord::InternalMetadata.create_table + end + + def internal_string_options_for_primary_key # :nodoc: + { primary_key: true } + end + + def assume_migrated_upto_version(version, migrations_paths) migrations_paths = Array(migrations_paths) version = version.to_i sm_table = quote_table_name(ActiveRecord::Migrator.schema_migrations_table_name) migrated = select_values("SELECT version FROM #{sm_table}").map(&:to_i) - paths = migrations_paths.map {|p| "#{p}/[0-9]*_*.rb" } + paths = migrations_paths.map { |p| "#{p}/[0-9]*_*.rb" } versions = Dir[*paths].map do |filename| - filename.split('/').last.split('_').first.to_i + filename.split("/").last.split("_").first.to_i end unless migrated.include?(version) execute "INSERT INTO #{sm_table} (version) VALUES ('#{version}')" end - inserted = Set.new - (versions - migrated).each do |v| - if inserted.include?(v) - raise "Duplicate migration #{v}. Please renumber your migrations to resolve the conflict." - elsif v < version - execute "INSERT INTO #{sm_table} (version) VALUES ('#{v}')" - inserted << v + inserting = (versions - migrated).select { |v| v < version } + if inserting.any? + if (duplicate = inserting.detect { |v| inserting.count(v) > 1 }) + raise "Duplicate migration #{duplicate}. Please renumber your migrations to resolve the conflict." + end + if supports_multi_insert? + execute insert_versions_sql(inserting) + else + inserting.each do |v| + execute insert_versions_sql(v) + end end end end def type_to_sql(type, limit = nil, precision = nil, scale = nil) #:nodoc: - if native = native_database_types[type.to_sym] + type = type.to_sym if type + if native = native_database_types[type] column_type_sql = (native.is_a?(Hash) ? native[:name] : native).dup if type == :decimal # ignore limit, use precision and scale @@ -917,18 +1085,19 @@ module ActiveRecord end # Given a set of columns and an ORDER BY clause, returns the columns for a SELECT DISTINCT. - # Both PostgreSQL and Oracle overrides this for custom DISTINCT syntax - they + # PostgreSQL, MySQL, and Oracle override this for custom DISTINCT syntax - they # require the order columns appear in the SELECT. # # columns_for_distinct("posts.id", ["posts.created_at desc"]) - def columns_for_distinct(columns, orders) #:nodoc: + # + def columns_for_distinct(columns, orders) # :nodoc: columns end # Adds timestamps (+created_at+ and +updated_at+) columns to +table_name+. - # Additional options (like <tt>null: false</tt>) are forwarded to #add_column. + # Additional options (like +:null+) are forwarded to #add_column. # - # add_timestamps(:suppliers, null: false) + # add_timestamps(:suppliers, null: true) # def add_timestamps(table_name, options = {}) options[:null] = false if options[:null].nil? @@ -950,16 +1119,19 @@ module ActiveRecord Table.new(table_name, base) end - def add_index_options(table_name, column_name, options = {}) #:nodoc: - column_names = Array(column_name) + def add_index_options(table_name, column_name, comment: nil, **options) # :nodoc: + if column_name.is_a?(String) && /\W/.match?(column_name) + column_names = column_name + else + column_names = Array(column_name) + end options.assert_valid_keys(:unique, :order, :name, :where, :length, :internal, :using, :algorithm, :type) index_type = options[:type].to_s if options.key?(:type) index_type ||= options[:unique] ? "UNIQUE" : "" index_name = options[:name].to_s if options.key?(:name) - index_name ||= index_name(table_name, column: column_names) - max_index_length = options.fetch(:internal, false) ? index_name_length : allowed_index_name_length + index_name ||= index_name(table_name, index_name_options(column_names)) if options.key?(:algorithm) algorithm = index_algorithms.fetch(options[:algorithm]) { @@ -973,63 +1145,90 @@ module ActiveRecord index_options = options[:where] ? " WHERE #{options[:where]}" : "" end - if index_name.length > max_index_length - raise ArgumentError, "Index name '#{index_name}' on table '#{table_name}' is too long; the limit is #{max_index_length} characters" - end - if table_exists?(table_name) && index_name_exists?(table_name, index_name, false) + validate_index_length!(table_name, index_name, options.fetch(:internal, false)) + + if data_source_exists?(table_name) && index_name_exists?(table_name, index_name, false) raise ArgumentError, "Index name '#{index_name}' on table '#{table_name}' already exists" end index_columns = quoted_columns_for_index(column_names, options).join(", ") - [index_name, index_type, index_columns, index_options, algorithm, using] + [index_name, index_type, index_columns, index_options, algorithm, using, comment] end - protected - def add_index_sort_order(option_strings, column_names, options = {}) - if options.is_a?(Hash) && order = options[:order] + def options_include_default?(options) + options.include?(:default) && !(options[:null] == false && options[:default].nil?) + end + + # Changes the comment for a table or removes it if +nil+. + def change_table_comment(table_name, comment) + raise NotImplementedError, "#{self.class} does not support changing table comments" + end + + # Changes the comment for a column or removes it if +nil+. + def change_column_comment(table_name, column_name, comment) #:nodoc: + raise NotImplementedError, "#{self.class} does not support changing column comments" + end + + private + + def add_index_sort_order(quoted_columns, **options) + if order = options[:order] case order when Hash - column_names.each {|name| option_strings[name] += " #{order[name].upcase}" if order.has_key?(name)} + order = order.symbolize_keys + quoted_columns.each { |name, column| column << " #{order[name].upcase}" if order[name].present? } when String - column_names.each {|name| option_strings[name] += " #{order.upcase}"} + quoted_columns.each { |name, column| column << " #{order.upcase}" if order.present? } end end - return option_strings + quoted_columns end # Overridden by the MySQL adapter for supporting index lengths - def quoted_columns_for_index(column_names, options = {}) - option_strings = Hash[column_names.map {|name| [name, '']}] - - # add index sort order if supported + def add_options_for_index_columns(quoted_columns, **options) if supports_index_sort_order? - option_strings = add_index_sort_order(option_strings, column_names, options) + quoted_columns = add_index_sort_order(quoted_columns, options) end - column_names.map {|name| quote_column_name(name) + option_strings[name]} + quoted_columns end - def options_include_default?(options) - options.include?(:default) && !(options[:null] == false && options[:default].nil?) + def quoted_columns_for_index(column_names, **options) + return [column_names] if column_names.is_a?(String) + + quoted_columns = Hash[column_names.map { |name| [name.to_sym, quote_column_name(name).dup] }] + add_options_for_index_columns(quoted_columns, options).values end def index_name_for_remove(table_name, options = {}) - index_name = index_name(table_name, options) + return options[:name] if can_remove_index_by_name?(options) - unless index_name_exists?(table_name, index_name, true) - if options.is_a?(Hash) && options.has_key?(:name) - options_without_column = options.dup - options_without_column.delete :column - index_name_without_column = index_name(table_name, options_without_column) + checks = [] - return index_name_without_column if index_name_exists?(table_name, index_name_without_column, false) - end + if options.is_a?(Hash) + checks << lambda { |i| i.name == options[:name].to_s } if options.key?(:name) + column_names = Array(options[:column]).map(&:to_s) + else + column_names = Array(options).map(&:to_s) + end - raise ArgumentError, "Index name '#{index_name}' on table '#{table_name}' does not exist" + if column_names.any? + checks << lambda { |i| i.columns.join("_and_") == column_names.join("_and_") } end - index_name + raise ArgumentError, "No name or columns specified" if checks.none? + + matching_indexes = indexes(table_name).select { |i| checks.all? { |check| check[i] } } + + if matching_indexes.count > 1 + raise ArgumentError, "Multiple indexes found on #{table_name} columns #{column_names}. " \ + "Specify an index name from #{matching_indexes.map(&:name).join(', ')}" + elsif matching_indexes.none? + raise ArgumentError, "No indexes found on #{table_name} with the options provided." + else + matching_indexes.first.name + end end def rename_table_indexes(table_name, new_name) @@ -1054,36 +1253,49 @@ module ActiveRecord end end - private - def create_table_definition(name, temporary = false, options = nil, as = nil) - TableDefinition.new native_database_types, name, temporary, options, as - end + def create_table_definition(*args) + TableDefinition.new(*args) + end - def create_alter_table(name) - AlterTable.new create_table_definition(name) - end + def create_alter_table(name) + AlterTable.new create_table_definition(name) + end - def foreign_key_name(table_name, options) # :nodoc: - identifier = "#{table_name}_#{options.fetch(:column)}_fk" - hashed_identifier = Digest::SHA256.hexdigest(identifier).first(10) - options.fetch(:name) do - "fk_rails_#{hashed_identifier}" + def index_name_options(column_names) + if column_names.is_a?(String) + column_names = column_names.scan(/\w+/).join("_") + end + + { column: column_names } end - end - def validate_index_length!(table_name, new_name) # :nodoc: - if new_name.length > allowed_index_name_length - raise ArgumentError, "Index name '#{new_name}' on table '#{table_name}' is too long; the limit is #{allowed_index_name_length} characters" + def foreign_key_name(table_name, options) + identifier = "#{table_name}_#{options.fetch(:column)}_fk" + hashed_identifier = Digest::SHA256.hexdigest(identifier).first(10) + options.fetch(:name) do + "fk_rails_#{hashed_identifier}" + end end - end - def extract_new_default_value(default_or_changes) - if default_or_changes.is_a?(Hash) && default_or_changes.has_key?(:from) && default_or_changes.has_key?(:to) - default_or_changes[:to] - else - default_or_changes + def validate_index_length!(table_name, new_name, internal = false) + max_index_length = internal ? index_name_length : allowed_index_name_length + + if new_name.length > max_index_length + raise ArgumentError, "Index name '#{new_name}' on table '#{table_name}' is too long; the limit is #{allowed_index_name_length} characters" + end + end + + def extract_new_default_value(default_or_changes) + if default_or_changes.is_a?(Hash) && default_or_changes.has_key?(:from) && default_or_changes.has_key?(:to) + default_or_changes[:to] + else + default_or_changes + end + end + + def can_remove_index_by_name?(options) + options.is_a?(Hash) && options.key?(:name) && options.except(:name, :algorithm).empty? end - end end end end diff --git a/activerecord/lib/active_record/connection_adapters/abstract/transaction.rb b/activerecord/lib/active_record/connection_adapters/abstract/transaction.rb index 295a7bed87..6bb072dd73 100644 --- a/activerecord/lib/active_record/connection_adapters/abstract/transaction.rb +++ b/activerecord/lib/active_record/connection_adapters/abstract/transaction.rb @@ -33,6 +33,7 @@ module ActiveRecord class NullTransaction #:nodoc: def initialize; end + def state; end def closed?; true; end def open?; false; end def joinable?; false; end @@ -40,7 +41,6 @@ module ActiveRecord end class Transaction #:nodoc: - attr_reader :connection, :state, :records, :savepoint_name attr_writer :joinable @@ -100,7 +100,6 @@ module ActiveRecord end class SavepointTransaction < Transaction - def initialize(connection, savepoint_name, options, *args) super(connection, options, *args) if options[:isolation] @@ -123,7 +122,6 @@ module ActiveRecord end class RealTransaction < Transaction - def initialize(connection, options, *args) super if options[:isolation] @@ -166,8 +164,13 @@ module ActiveRecord def commit_transaction transaction = @stack.last - transaction.before_commit_records - @stack.pop + + begin + transaction.before_commit_records + ensure + @stack.pop + end + transaction.commit transaction.commit_records end @@ -182,11 +185,14 @@ module ActiveRecord transaction = begin_transaction options yield rescue Exception => error - rollback_transaction if transaction + if transaction + rollback_transaction + after_failure_actions(transaction, error) + end raise ensure unless error - if Thread.current.status == 'aborting' + if Thread.current.status == "aborting" rollback_transaction if transaction else begin @@ -208,7 +214,15 @@ module ActiveRecord end private + NULL_TRANSACTION = NullTransaction.new + + # Deallocate invalidated prepared statements outside of the transaction + def after_failure_actions(transaction, error) + return unless transaction.is_a?(RealTransaction) + return unless error.is_a?(ActiveRecord::PreparedStatementCacheExpired) + @connection.clear_cache! + end end end end diff --git a/activerecord/lib/active_record/connection_adapters/abstract_adapter.rb b/activerecord/lib/active_record/connection_adapters/abstract_adapter.rb index 6d3a21a3dc..4046b3829d 100644 --- a/activerecord/lib/active_record/connection_adapters/abstract_adapter.rb +++ b/activerecord/lib/active_record/connection_adapters/abstract_adapter.rb @@ -1,11 +1,11 @@ -require 'active_record/type' -require 'active_support/core_ext/benchmark' -require 'active_record/connection_adapters/schema_cache' -require 'active_record/connection_adapters/sql_type_metadata' -require 'active_record/connection_adapters/abstract/schema_dumper' -require 'active_record/connection_adapters/abstract/schema_creation' -require 'arel/collectors/bind' -require 'arel/collectors/sql_string' +require "active_record/type" +require "active_record/connection_adapters/determine_if_preparable_visitor" +require "active_record/connection_adapters/schema_cache" +require "active_record/connection_adapters/sql_type_metadata" +require "active_record/connection_adapters/abstract/schema_dumper" +require "active_record/connection_adapters/abstract/schema_creation" +require "arel/collectors/bind" +require "arel/collectors/sql_string" module ActiveRecord module ConnectionAdapters # :nodoc: @@ -14,7 +14,7 @@ module ActiveRecord autoload :Column autoload :ConnectionSpecification - autoload_at 'active_record/connection_adapters/abstract/schema_definitions' do + autoload_at "active_record/connection_adapters/abstract/schema_definitions" do autoload :IndexDefinition autoload :ColumnDefinition autoload :ChangeColumnDefinition @@ -22,14 +22,14 @@ module ActiveRecord autoload :TableDefinition autoload :Table autoload :AlterTable + autoload :ReferenceDefinition end - autoload_at 'active_record/connection_adapters/abstract/connection_pool' do + autoload_at "active_record/connection_adapters/abstract/connection_pool" do autoload :ConnectionHandler - autoload :ConnectionManagement end - autoload_under 'abstract' do + autoload_under "abstract" do autoload :SchemaStatements autoload :DatabaseStatements autoload :DatabaseLimits @@ -39,7 +39,7 @@ module ActiveRecord autoload :Savepoints end - autoload_at 'active_record/connection_adapters/abstract/transaction' do + autoload_at "active_record/connection_adapters/abstract/transaction" do autoload :TransactionManager autoload :NullTransaction autoload :RealTransaction @@ -51,27 +51,28 @@ module ActiveRecord # related classes form the abstraction layer which makes this possible. # An AbstractAdapter represents a connection to a database, and provides an # abstract interface for database-specific functionality such as establishing - # a connection, escaping values, building the right SQL fragments for ':offset' - # and ':limit' options, etc. + # a connection, escaping values, building the right SQL fragments for +:offset+ + # and +:limit+ options, etc. # # All the concrete database adapters follow the interface laid down in this class. - # ActiveRecord::Base.connection returns an AbstractAdapter object, which + # {ActiveRecord::Base.connection}[rdoc-ref:ConnectionHandling#connection] returns an AbstractAdapter object, which # you can use. # # Most of the methods in the adapter are useful during migrations. Most - # notably, the instance methods provided by SchemaStatement are very useful. + # notably, the instance methods provided by SchemaStatements are very useful. class AbstractAdapter - ADAPTER_NAME = 'Abstract'.freeze + ADAPTER_NAME = "Abstract".freeze + include ActiveSupport::Callbacks + define_callbacks :checkout, :checkin + include Quoting, DatabaseStatements, SchemaStatements include DatabaseLimits include QueryCache - include ActiveSupport::Callbacks include ColumnDumper + include Savepoints SIMPLE_INT = /\A\d+\z/ - define_callbacks :checkout, :checkin - attr_accessor :visitor, :pool attr_reader :schema_cache, :owner, :logger alias :in_use? :owner @@ -94,22 +95,42 @@ module ActiveRecord attr_reader :prepared_statements - def initialize(connection, logger = nil, pool = nil) #:nodoc: + def initialize(connection, logger = nil, config = {}) # :nodoc: super() @connection = connection @owner = nil @instrumenter = ActiveSupport::Notifications.instrumenter @logger = logger - @pool = pool + @config = config + @pool = nil @schema_cache = SchemaCache.new self - @visitor = nil - @prepared_statements = false + @quoted_column_names, @quoted_table_names = {}, {} + @visitor = arel_visitor + + if self.class.type_cast_config_to_boolean(config.fetch(:prepared_statements) { true }) + @prepared_statements = true + @visitor.extend(DetermineIfPreparableVisitor) + else + @prepared_statements = false + end + end + + class Version + include Comparable + + def initialize(version_string) + @version = version_string.split(".").map(&:to_i) + end + + def <=>(version_string) + @version <=> version_string.split(".").map(&:to_i) + end end class BindCollector < Arel::Collectors::Bind def compile(bvs, conn) - casted_binds = conn.prepare_binds_for_database(bvs) + casted_binds = bvs.map(&:value_for_database) super(casted_binds.map { |value| conn.quote(value) }) end end @@ -128,20 +149,32 @@ module ActiveRecord end end + def arel_visitor # :nodoc: + Arel::Visitors::ToSql.new(self) + end + def valid_type?(type) - true + false end def schema_creation SchemaCreation.new self end + # Returns an array of +Column+ objects for the table specified by +table_name+. + def columns(table_name) # :nodoc: + table_name = table_name.to_s + column_definitions(table_name).map do |field| + new_column_from_field(table_name, field) + end + end + # this method must only be called while holding connection pool's mutex def lease if in_use? - msg = 'Cannot lease connection, ' + msg = "Cannot lease connection, " if @owner == Thread.current - msg << 'it is already leased by the current thread.' + msg << "it is already leased by the current thread." else msg << "it is already in use by a different thread: #{@owner}. " << "Current thread: #{Thread.current}." @@ -159,7 +192,30 @@ module ActiveRecord # this method must only be called while holding connection pool's mutex def expire - @owner = nil + if in_use? + if @owner != Thread.current + raise ActiveRecordError, "Cannot expire connection, " << + "it is owned by a different thread: #{@owner}. " << + "Current thread: #{Thread.current}." + end + + @owner = nil + else + raise ActiveRecordError, "Cannot expire connection, it is not currently leased." + end + end + + # this method must only be called while holding connection pool's mutex (and a desire for segfaults) + def steal! # :nodoc: + if in_use? + if @owner != Thread.current + pool.send :remove_connection_from_thread_cache, self, @owner + + @owner = Thread.current + end + else + raise ActiveRecordError, "Cannot steal connection, it is not currently leased." + end end def unprepared_statement @@ -201,6 +257,11 @@ module ActiveRecord false end + # Does this adapter support application-enforced advisory locking? + def supports_advisory_locks? + false + end + # Should primary key values be selected from their corresponding # sequence before the insert statement? If true, next_sequence_value # is called before each insert to set the record's primary key. @@ -218,6 +279,11 @@ module ActiveRecord false end + # Does this adapter support expression indices? + def supports_expression_index? + false + end + # Does this adapter support explain? def supports_explain? false @@ -254,6 +320,26 @@ module ActiveRecord false end + # Does this adapter support json data type? + def supports_json? + false + end + + # Does this adapter support metadata comments on database objects (tables, columns, indexes)? + def supports_comments? + false + end + + # Can comments for tables, columns, and indexes be specified in create/alter table statements? + def supports_comments_in_create? + false + end + + # Does this adapter support multi-value insert? + def supports_multi_insert? + true + end + # This is meant to be implemented by the adapters that support extensions def disable_extension(name) end @@ -262,6 +348,20 @@ module ActiveRecord def enable_extension(name) end + # This is meant to be implemented by the adapters that support advisory + # locks + # + # Return true if we got the lock, otherwise false + def get_advisory_lock(lock_id) # :nodoc: + end + + # This is meant to be implemented by the adapters that support advisory + # locks. + # + # Return true if we released the lock, otherwise false + def release_advisory_lock(lock_id) # :nodoc: + end + # A list of extensions, to be filled in by adapters that support them. def extensions [] @@ -272,12 +372,6 @@ module ActiveRecord {} end - # Returns a bind substitution value given a bind +column+ - # NOTE: The column param is currently being used by the sqlserver-adapter - def substitute_at(column, _unused = 0) - Arel::Nodes::BindParam.new - end - # REFERENTIAL INTEGRITY ==================================== # Override to turn off referential integrity while executing <tt>&block</tt>. @@ -331,14 +425,14 @@ module ActiveRecord end # Checks whether the connection to the database is still active (i.e. not stale). - # This is done under the hood by calling <tt>active?</tt>. If the connection + # This is done under the hood by calling #active?. If the connection # is no longer active, then this method will reconnect to the database. def verify!(*ignored) reconnect! unless active? end # Provides access to the underlying database driver for this adapter. For - # example, this method returns a Mysql object in case of MysqlAdapter, + # example, this method returns a Mysql2::Client object in case of Mysql2Adapter, # and a PGconn object in case of PostgreSQLAdapter. # # This is useful for when you need to call a proprietary method such as @@ -347,27 +441,15 @@ module ActiveRecord @connection end - def create_savepoint(name = nil) - end - - def release_savepoint(name = nil) - end - - def case_sensitive_modifier(node, table_attribute) - node - end - def case_sensitive_comparison(table, attribute, column, value) - table_attr = table[attribute] - value = case_sensitive_modifier(value, table_attr) unless value.nil? - table_attr.eq(value) + table[attribute].eq(Arel::Nodes::BindParam.new) end def case_insensitive_comparison(table, attribute, column, value) if can_perform_case_insensitive_comparison_for?(column) - table[attribute].lower.eq(table.lower(value)) + table[attribute].lower.eq(table.lower(Arel::Nodes::BindParam.new)) else - case_sensitive_comparison(table, attribute, column, value) + table[attribute].eq(Arel::Nodes::BindParam.new) end end @@ -376,10 +458,6 @@ module ActiveRecord end private :can_perform_case_insensitive_comparison_for? - def current_savepoint_name - current_transaction.savepoint_name - end - # Check the connection back in to the connection pool def close pool.checkin self @@ -391,8 +469,8 @@ module ActiveRecord end end - def new_column(name, default, sql_type_metadata = nil, null = true, default_function = nil, collation = nil) - Column.new(name, default, sql_type_metadata, null, default_function, collation) + def new_column(name, default, sql_type_metadata, null, table_name, default_function = nil, collation = nil) # :nodoc: + Column.new(name, default, sql_type_metadata, null, table_name, default_function, collation) end def lookup_cast_type(sql_type) # :nodoc: @@ -403,116 +481,140 @@ module ActiveRecord visitor.accept(node, collector).value end - protected - - def initialize_type_map(m) # :nodoc: - register_class_with_limit m, %r(boolean)i, Type::Boolean - register_class_with_limit m, %r(char)i, Type::String - register_class_with_limit m, %r(binary)i, Type::Binary - register_class_with_limit m, %r(text)i, Type::Text - register_class_with_precision m, %r(date)i, Type::Date - register_class_with_precision m, %r(time)i, Type::Time - register_class_with_precision m, %r(datetime)i, Type::DateTime - register_class_with_limit m, %r(float)i, Type::Float - register_class_with_limit m, %r(int)i, Type::Integer - - m.alias_type %r(blob)i, 'binary' - m.alias_type %r(clob)i, 'text' - m.alias_type %r(timestamp)i, 'datetime' - m.alias_type %r(numeric)i, 'decimal' - m.alias_type %r(number)i, 'decimal' - m.alias_type %r(double)i, 'float' - - m.register_type(%r(decimal)i) do |sql_type| - scale = extract_scale(sql_type) - precision = extract_precision(sql_type) - - if scale == 0 - # FIXME: Remove this class as well - Type::DecimalWithoutScale.new(precision: precision) - else - Type::Decimal.new(precision: precision, scale: scale) + def combine_bind_parameters( + from_clause: [], + join_clause: [], + where_clause: [], + having_clause: [], + limit: nil, + offset: nil + ) # :nodoc: + result = from_clause + join_clause + where_clause + having_clause + if limit + result << limit + end + if offset + result << offset + end + result + end + + private + + def initialize_type_map(m) + register_class_with_limit m, %r(boolean)i, Type::Boolean + register_class_with_limit m, %r(char)i, Type::String + register_class_with_limit m, %r(binary)i, Type::Binary + register_class_with_limit m, %r(text)i, Type::Text + register_class_with_precision m, %r(date)i, Type::Date + register_class_with_precision m, %r(time)i, Type::Time + register_class_with_precision m, %r(datetime)i, Type::DateTime + register_class_with_limit m, %r(float)i, Type::Float + register_class_with_limit m, %r(int)i, Type::Integer + + m.alias_type %r(blob)i, "binary" + m.alias_type %r(clob)i, "text" + m.alias_type %r(timestamp)i, "datetime" + m.alias_type %r(numeric)i, "decimal" + m.alias_type %r(number)i, "decimal" + m.alias_type %r(double)i, "float" + + m.register_type(%r(decimal)i) do |sql_type| + scale = extract_scale(sql_type) + precision = extract_precision(sql_type) + + if scale == 0 + # FIXME: Remove this class as well + Type::DecimalWithoutScale.new(precision: precision) + else + Type::Decimal.new(precision: precision, scale: scale) + end end end - end - def reload_type_map # :nodoc: - type_map.clear - initialize_type_map(type_map) - end + def reload_type_map + type_map.clear + initialize_type_map(type_map) + end - def register_class_with_limit(mapping, key, klass) # :nodoc: - mapping.register_type(key) do |*args| - limit = extract_limit(args.last) - klass.new(limit: limit) + def register_class_with_limit(mapping, key, klass) + mapping.register_type(key) do |*args| + limit = extract_limit(args.last) + klass.new(limit: limit) + end end - end - def register_class_with_precision(mapping, key, klass) # :nodoc: - mapping.register_type(key) do |*args| - precision = extract_precision(args.last) - klass.new(precision: precision) + def register_class_with_precision(mapping, key, klass) + mapping.register_type(key) do |*args| + precision = extract_precision(args.last) + klass.new(precision: precision) + end end - end - def extract_scale(sql_type) # :nodoc: - case sql_type + def extract_scale(sql_type) + case sql_type when /\((\d+)\)/ then 0 when /\((\d+)(,(\d+))\)/ then $3.to_i + end end - end - def extract_precision(sql_type) # :nodoc: - $1.to_i if sql_type =~ /\((\d+)(,\d+)?\)/ - end - - def extract_limit(sql_type) # :nodoc: - case sql_type - when /^bigint/i - 8 - when /\((.*)\)/ - $1.to_i + def extract_precision(sql_type) + $1.to_i if sql_type =~ /\((\d+)(,\d+)?\)/ end - end - def translate_exception_class(e, sql) - begin - message = "#{e.class.name}: #{e.message}: #{sql}" - rescue Encoding::CompatibilityError - message = "#{e.class.name}: #{e.message.force_encoding sql.encoding}: #{sql}" + def extract_limit(sql_type) + case sql_type + when /^bigint/i + 8 + when /\((.*)\)/ + $1.to_i + end end - exception = translate_exception(e, message) - exception.set_backtrace e.backtrace - exception - end + def translate_exception_class(e, sql) + begin + message = "#{e.class.name}: #{e.message}: #{sql}" + rescue Encoding::CompatibilityError + message = "#{e.class.name}: #{e.message.force_encoding sql.encoding}: #{sql}" + end - def log(sql, name = "SQL", binds = [], statement_name = nil) - @instrumenter.instrument( - "sql.active_record", - :sql => sql, - :name => name, - :connection_id => object_id, - :statement_name => statement_name, - :binds => binds) { yield } - rescue => e - raise translate_exception_class(e, sql) - end + exception = translate_exception(e, message) + exception.set_backtrace e.backtrace + exception + end - def translate_exception(exception, message) - # override in derived class - ActiveRecord::StatementInvalid.new(message, exception) - end + def log(sql, name = "SQL", binds = [], type_casted_binds = [], statement_name = nil) # :doc: + @instrumenter.instrument( + "sql.active_record", + sql: sql, + name: name, + binds: binds, + type_casted_binds: type_casted_binds, + statement_name: statement_name, + connection_id: object_id) { yield } + rescue => e + raise translate_exception_class(e, sql) + end - def without_prepared_statement?(binds) - !prepared_statements || binds.empty? - end + def translate_exception(exception, message) + # override in derived class + case exception + when RuntimeError + exception + else + ActiveRecord::StatementInvalid.new(message) + end + end - def column_for(table_name, column_name) # :nodoc: - column_name = column_name.to_s - columns(table_name).detect { |c| c.name == column_name } || - raise(ActiveRecordError, "No such column: #{table_name}.#{column_name}") - end + def without_prepared_statement?(binds) + !prepared_statements || binds.empty? + end + + def column_for(table_name, column_name) + column_name = column_name.to_s + columns(table_name).detect { |c| c.name == column_name } || + raise(ActiveRecordError, "No such column: #{table_name}.#{column_name}") + end end end end diff --git a/activerecord/lib/active_record/connection_adapters/abstract_mysql_adapter.rb b/activerecord/lib/active_record/connection_adapters/abstract_mysql_adapter.rb index 2027492f29..1c3d10c15d 100644 --- a/activerecord/lib/active_record/connection_adapters/abstract_mysql_adapter.rb +++ b/activerecord/lib/active_record/connection_adapters/abstract_mysql_adapter.rb @@ -1,286 +1,91 @@ -require 'active_support/core_ext/string/strip' +require "active_record/connection_adapters/abstract_adapter" +require "active_record/connection_adapters/statement_pool" +require "active_record/connection_adapters/mysql/column" +require "active_record/connection_adapters/mysql/explain_pretty_printer" +require "active_record/connection_adapters/mysql/quoting" +require "active_record/connection_adapters/mysql/schema_creation" +require "active_record/connection_adapters/mysql/schema_definitions" +require "active_record/connection_adapters/mysql/schema_dumper" +require "active_record/connection_adapters/mysql/type_metadata" + +require "active_support/core_ext/string/strip" module ActiveRecord module ConnectionAdapters class AbstractMysqlAdapter < AbstractAdapter - include Savepoints - - module ColumnMethods - def primary_key(name, type = :primary_key, **options) - options[:auto_increment] = true if type == :bigint - super - end - end - - class ColumnDefinition < ActiveRecord::ConnectionAdapters::ColumnDefinition - attr_accessor :charset - end - - class TableDefinition < ActiveRecord::ConnectionAdapters::TableDefinition - include ColumnMethods - - def new_column_definition(name, type, options) # :nodoc: - column = super - case column.type - when :primary_key - column.type = :integer - column.auto_increment = true - end - column.charset = options[:charset] - column - end - - private - - def create_column_definition(name, type) - ColumnDefinition.new(name, type) - end - end - - class Table < ActiveRecord::ConnectionAdapters::Table - include ColumnMethods - end - - class SchemaCreation < AbstractAdapter::SchemaCreation - private - - def visit_DropForeignKey(name) - "DROP FOREIGN KEY #{name}" - end - - def visit_TableDefinition(o) - name = o.name - create_sql = "CREATE#{' TEMPORARY' if o.temporary} TABLE #{quote_table_name(name)} " - - statements = o.columns.map { |c| accept c } - statements.concat(o.indexes.map { |column_name, options| index_in_create(name, column_name, options) }) - - create_sql << "(#{statements.join(', ')}) " if statements.present? - create_sql << "#{o.options}" - create_sql << " AS #{@conn.to_sql(o.as)}" if o.as - create_sql - end - - def visit_AddColumnDefinition(o) - add_column_position!(super, column_options(o.column)) - end - - def visit_ChangeColumnDefinition(o) - change_column_sql = "CHANGE #{quote_column_name(o.name)} #{accept(o.column)}" - add_column_position!(change_column_sql, column_options(o.column)) - end - - def column_options(o) - column_options = super - column_options[:charset] = o.charset - column_options - end - - def add_column_options!(sql, options) - if options[:charset] - sql << " CHARACTER SET #{options[:charset]}" - end - if options[:collation] - sql << " COLLATE #{options[:collation]}" - end - super - end - - def add_column_position!(sql, options) - if options[:first] - sql << " FIRST" - elsif options[:after] - sql << " AFTER #{quote_column_name(options[:after])}" - end - sql - end - - def index_in_create(table_name, column_name, options) - index_name, index_type, index_columns, _, _, index_using = @conn.add_index_options(table_name, column_name, options) - "#{index_type} INDEX #{quote_column_name(index_name)} #{index_using} (#{index_columns}) " - end - end + include MySQL::Quoting + include MySQL::ColumnDumper def update_table_definition(table_name, base) # :nodoc: - Table.new(table_name, base) - end - - def schema_creation - SchemaCreation.new self - end - - def column_spec_for_primary_key(column) - spec = {} - if column.auto_increment? - spec[:id] = ':bigint' if column.bigint? - return if spec.empty? - else - spec[:id] = column.type.inspect - spec.merge!(prepare_column_options(column).delete_if { |key, _| [:name, :type, :null].include?(key) }) - end - spec + MySQL::Table.new(table_name, base) end - private - - def schema_limit(column) - super unless column.type == :boolean + def schema_creation # :nodoc: + MySQL::SchemaCreation.new(self) end - def schema_precision(column) - super unless /time/ === column.sql_type && column.precision == 0 - end - - def schema_collation(column) - if column.collation && table_name = column.instance_variable_get(:@table_name) - @collation_cache ||= {} - @collation_cache[table_name] ||= select_one("SHOW TABLE STATUS LIKE '#{table_name}'")["Collation"] - column.collation.inspect if column.collation != @collation_cache[table_name] - end - end - - public - - class Column < ConnectionAdapters::Column # :nodoc: - delegate :strict, :extra, to: :sql_type_metadata, allow_nil: true - - def initialize(*) - super - assert_valid_default(default) - extract_default - end - - def extract_default - if blob_or_text_column? - @default = null || strict ? nil : '' - elsif missing_default_forged_as_empty_string?(default) - @default = nil - end - end - - def has_default? - return false if blob_or_text_column? # MySQL forbids defaults on blob and text columns - super - end - - def blob_or_text_column? - sql_type =~ /blob/i || type == :text - end - - def case_sensitive? - collation && !collation.match(/_ci$/) - end - - def auto_increment? - extra == 'auto_increment' - end - - private - - # MySQL misreports NOT NULL column default when none is given. - # We can't detect this for columns which may have a legitimate '' - # default (string) but we can for others (integer, datetime, boolean, - # and the rest). - # - # Test whether the column has default '', is not null, and is not - # a type allowing default ''. - def missing_default_forged_as_empty_string?(default) - type != :string && !null && default == '' - end - - def assert_valid_default(default) - if blob_or_text_column? && default.present? - raise ArgumentError, "#{type} columns cannot have a default value: #{default.inspect}" - end - end - end - - class MysqlTypeMetadata < DelegateClass(SqlTypeMetadata) # :nodoc: - attr_reader :extra, :strict - - def initialize(type_metadata, extra: "", strict: false) - super(type_metadata) - @type_metadata = type_metadata - @extra = extra - @strict = strict - end - - def ==(other) - other.is_a?(MysqlTypeMetadata) && - attributes_for_hash == other.attributes_for_hash - end - alias eql? == - - def hash - attributes_for_hash.hash - end - - protected - - def attributes_for_hash - [self.class, @type_metadata, extra, strict] - end + def arel_visitor # :nodoc: + Arel::Visitors::MySQL.new(self) end ## # :singleton-method: - # By default, the MysqlAdapter will consider all columns of type <tt>tinyint(1)</tt> - # as boolean. If you wish to disable this emulation (which was the default - # behavior in versions 0.13.1 and earlier) you can add the following line + # By default, the Mysql2Adapter will consider all columns of type <tt>tinyint(1)</tt> + # as boolean. If you wish to disable this emulation you can add the following line # to your application.rb file: # - # ActiveRecord::ConnectionAdapters::Mysql[2]Adapter.emulate_booleans = false + # ActiveRecord::ConnectionAdapters::Mysql2Adapter.emulate_booleans = false class_attribute :emulate_booleans self.emulate_booleans = true - LOST_CONNECTION_ERROR_MESSAGES = [ - "Server shutdown in progress", - "Broken pipe", - "Lost connection to MySQL server during query", - "MySQL server has gone away" ] - - QUOTED_TRUE, QUOTED_FALSE = '1', '0' - NATIVE_DATABASE_TYPES = { - :primary_key => "int(11) auto_increment PRIMARY KEY", - :string => { :name => "varchar", :limit => 255 }, - :text => { :name => "text" }, - :integer => { :name => "int", :limit => 4 }, - :float => { :name => "float" }, - :decimal => { :name => "decimal" }, - :datetime => { :name => "datetime" }, - :time => { :name => "time" }, - :date => { :name => "date" }, - :binary => { :name => "blob" }, - :boolean => { :name => "tinyint", :limit => 1 } + primary_key: "bigint auto_increment PRIMARY KEY", + string: { name: "varchar", limit: 255 }, + text: { name: "text", limit: 65535 }, + integer: { name: "int", limit: 4 }, + float: { name: "float" }, + decimal: { name: "decimal" }, + datetime: { name: "datetime" }, + time: { name: "time" }, + date: { name: "date" }, + binary: { name: "blob", limit: 65535 }, + boolean: { name: "tinyint", limit: 1 }, + json: { name: "json" }, } INDEX_TYPES = [:fulltext, :spatial] INDEX_USINGS = [:btree, :hash] - # FIXME: Make the first parameter more similar for the two adapters + class StatementPool < ConnectionAdapters::StatementPool + private def dealloc(stmt) + stmt[:stmt].close + end + end + def initialize(connection, logger, connection_options, config) - super(connection, logger) - @connection_options, @config = connection_options, config - @quoted_column_names, @quoted_table_names = {}, {} + super(connection, logger, config) - @visitor = Arel::Visitors::MySQL.new self + @statements = StatementPool.new(self.class.type_cast_config_to_integer(config[:statement_limit])) - if self.class.type_cast_config_to_boolean(config.fetch(:prepared_statements) { true }) - @prepared_statements = true - else - @prepared_statements = false + if version < "5.0.0" + raise "Your version of MySQL (#{full_version.match(/^\d+\.\d+\.\d+/)[0]}) is too old. Active Record supports MySQL >= 5.0." end end - MAX_INDEX_LENGTH_FOR_CHARSETS_OF_4BYTES_MAXLEN = 191 - CHARSETS_OF_4BYTES_MAXLEN = ['utf8mb4', 'utf16', 'utf16le', 'utf32'] - def initialize_schema_migrations_table - if CHARSETS_OF_4BYTES_MAXLEN.include?(charset) - ActiveRecord::SchemaMigration.create_table(MAX_INDEX_LENGTH_FOR_CHARSETS_OF_4BYTES_MAXLEN) - else - ActiveRecord::SchemaMigration.create_table - end + CHARSETS_OF_4BYTES_MAXLEN = ["utf8mb4", "utf16", "utf16le", "utf32"] + + def internal_string_options_for_primary_key # :nodoc: + super.tap { |options| + options[:collation] = collation.sub(/\A[^_]+/, "utf8") if CHARSETS_OF_4BYTES_MAXLEN.include?(charset) + } + end + + def version #:nodoc: + @version ||= Version.new(full_version.match(/^\d+\.\d+\.\d+/)[0]) + end + + def mariadb? # :nodoc: + /mariadb/i.match?(full_version) end # Returns true, since this connection adapter supports migrations. @@ -296,18 +101,24 @@ module ActiveRecord true end + # Returns true, since this connection adapter supports prepared statement + # caching. + def supports_statement_cache? + true + end + # Technically MySQL allows to create indexes with the sort order syntax # but at the moment (5.5) it doesn't yet implement them def supports_index_sort_order? 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 + true + end + + def supports_explain? + true end def supports_indexes_in_create? @@ -319,11 +130,27 @@ module ActiveRecord end def supports_views? - version[0] >= 5 + true end def supports_datetime_with_precision? - (version[0] == 5 && version[1] >= 6) || version[0] >= 6 + if mariadb? + version >= "5.3.0" + else + version >= "5.6.4" + end + end + + def supports_advisory_locks? + true + end + + def get_advisory_lock(lock_name, timeout = 0) # :nodoc: + select_value("SELECT GET_LOCK(#{quote(lock_name)}, #{timeout})") == 1 + end + + def release_advisory_lock(lock_name) # :nodoc: + select_value("SELECT RELEASE_LOCK(#{quote(lock_name)})") == 1 end def native_database_types @@ -331,7 +158,7 @@ module ActiveRecord end def index_algorithms - { default: 'ALGORITHM = DEFAULT', copy: 'ALGORITHM = COPY', inplace: 'ALGORITHM = INPLACE' } + { default: "ALGORITHM = DEFAULT", copy: "ALGORITHM = COPY", inplace: "ALGORITHM = INPLACE" } end # HELPER METHODS =========================================== @@ -342,8 +169,8 @@ module ActiveRecord raise NotImplementedError end - def new_column(field, default, sql_type_metadata = nil, null = true, default_function = nil, collation = nil) # :nodoc: - Column.new(field, default, sql_type_metadata, null, default_function, collation) + def new_column(*args) #:nodoc: + MySQL::Column.new(*args) end # Must return the MySQL error number from the exception, if the exception has an @@ -352,40 +179,6 @@ module ActiveRecord raise NotImplementedError end - # QUOTING ================================================== - - def _quote(value) # :nodoc: - if value.is_a?(Type::Binary::Data) - "x'#{value.hex}'" - else - super - end - end - - def quote_column_name(name) #:nodoc: - @quoted_column_names[name] ||= "`#{name.to_s.gsub('`', '``')}`" - end - - def quote_table_name(name) #:nodoc: - @quoted_table_names[name] ||= quote_column_name(name).gsub('.', '`.`') - end - - def quoted_true - QUOTED_TRUE - end - - def unquoted_true - 1 - end - - def quoted_false - QUOTED_FALSE - end - - def unquoted_false - 0 - end - # REFERENTIAL INTEGRITY ==================================== def disable_referential_integrity #:nodoc: @@ -399,32 +192,43 @@ module ActiveRecord end end + # CONNECTION MANAGEMENT ==================================== + + # Clears the prepared statements cache. + def clear_cache! + reload_type_map + @statements.clear + end + #-- # DATABASE STATEMENTS ====================================== #++ - def clear_cache! - super - reload_type_map + def explain(arel, binds = []) + sql = "EXPLAIN #{to_sql(arel, binds)}" + start = Time.now + result = exec_query(sql, "EXPLAIN", binds) + elapsed = Time.now - start + + MySQL::ExplainPrettyPrinter.new.pp(result, elapsed) end # Executes the SQL statement in the context of this connection. def execute(sql, name = nil) - log(sql, name) { @connection.query(sql) } + log(sql, name) do + ActiveSupport::Dependencies.interlock.permit_concurrent_loads do + @connection.query(sql) + end + end end - # MysqlAdapter has to free a result after using it, so we use this method to write - # stuff in an abstract way without concerning ourselves about whether it needs to be - # explicitly freed or not. - def execute_and_free(sql, name = nil) #:nodoc: + # Mysql2Adapter doesn't have to free a result after using it, but we use this method + # to write stuff in an abstract way without concerning ourselves about whether it + # needs to be explicitly freed or not. + def execute_and_free(sql, name = nil) # :nodoc: yield execute(sql, name) end - def update_sql(sql, name = nil) #:nodoc: - super - @connection.affected_rows - end - def begin_db_transaction execute "BEGIN" end @@ -445,7 +249,7 @@ module ActiveRecord # In the simple case, MySQL allows us to place JOINs directly into the UPDATE # query. However, this does not allow for LIMIT, OFFSET and ORDER. To support # these, we must use a subquery. - def join_to_update(update, select) #:nodoc: + def join_to_update(update, select, key) # :nodoc: if select.limit || select.offset || select.orders.any? super else @@ -478,9 +282,9 @@ module ActiveRecord # 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]}`" + execute "CREATE DATABASE #{quote_table_name(name)} DEFAULT CHARACTER SET #{quote_table_name(options[:charset] || 'utf8')} COLLATE #{quote_table_name(options[:collation])}" else - execute "CREATE DATABASE `#{name}` DEFAULT CHARACTER SET `#{options[:charset] || 'utf8'}`" + execute "CREATE DATABASE #{quote_table_name(name)} DEFAULT CHARACTER SET #{quote_table_name(options[:charset] || 'utf8')}" end end @@ -489,91 +293,125 @@ module ActiveRecord # Example: # drop_database('sebastian_development') def drop_database(name) #:nodoc: - execute "DROP DATABASE IF EXISTS `#{name}`" + execute "DROP DATABASE IF EXISTS #{quote_table_name(name)}" end def current_database - select_value 'SELECT DATABASE() as db' + select_value "SELECT DATABASE() as db" end # Returns the database character set. def charset - show_variable 'character_set_database' + show_variable "character_set_database" end # Returns the database collation strategy. def collation - show_variable 'collation_database' + show_variable "collation_database" end - def tables(name = nil, database = nil, like = nil) #:nodoc: - sql = "SHOW TABLES " - sql << "IN #{quote_table_name(database)} " if database - sql << "LIKE #{quote(like)}" if like + def tables # :nodoc: + sql = "SELECT table_name FROM information_schema.tables WHERE table_type = 'BASE TABLE'" + sql << " AND table_schema = #{quote(@config[:database])}" - execute_and_free(sql, 'SCHEMA') do |result| - result.collect(&:first) - end + select_values(sql, "SCHEMA") end - def truncate(table_name, name = nil) - execute "TRUNCATE TABLE #{quote_table_name(table_name)}", name + def views # :nodoc: + select_values("SHOW FULL TABLES WHERE table_type = 'VIEW'", "SCHEMA") end - def table_exists?(name) - return false unless name.present? - return true if tables(nil, nil, name).any? + def data_sources # :nodoc: + sql = "SELECT table_name FROM information_schema.tables " + sql << "WHERE table_schema = #{quote(@config[:database])}" - name = name.to_s - schema, table = name.split('.', 2) + select_values(sql, "SCHEMA") + end - unless table # A table was provided without a schema - table = schema - schema = nil - end + def table_exists?(table_name) # :nodoc: + return false unless table_name.present? + + schema, name = extract_schema_qualified_name(table_name) + + sql = "SELECT table_name FROM information_schema.tables WHERE table_type = 'BASE TABLE'" + sql << " AND table_schema = #{quote(schema)} AND table_name = #{quote(name)}" + + select_values(sql, "SCHEMA").any? + end + + def data_source_exists?(table_name) # :nodoc: + return false unless table_name.present? + + schema, name = extract_schema_qualified_name(table_name) + + sql = "SELECT table_name FROM information_schema.tables " + sql << "WHERE table_schema = #{quote(schema)} AND table_name = #{quote(name)}" + + select_values(sql, "SCHEMA").any? + end + + def view_exists?(view_name) # :nodoc: + return false unless view_name.present? + + schema, name = extract_schema_qualified_name(view_name) + + sql = "SELECT table_name FROM information_schema.tables WHERE table_type = 'VIEW'" + sql << " AND table_schema = #{quote(schema)} AND table_name = #{quote(name)}" - tables(nil, schema, table).any? + select_values(sql, "SCHEMA").any? + end + + def truncate(table_name, name = nil) + execute "TRUNCATE TABLE #{quote_table_name(table_name)}", name end # Returns an array of indexes for the given table. def indexes(table_name, name = nil) #:nodoc: indexes = [] current_index = nil - execute_and_free("SHOW KEYS FROM #{quote_table_name(table_name)}", 'SCHEMA') do |result| + execute_and_free("SHOW KEYS FROM #{quote_table_name(table_name)}", "SCHEMA") do |result| each_hash(result) do |row| if current_index != row[:Key_name] - next if row[:Key_name] == 'PRIMARY' # skip the primary key + next if row[:Key_name] == "PRIMARY" # skip the primary key current_index = row[:Key_name] mysql_index_type = row[:Index_type].downcase.to_sym index_type = INDEX_TYPES.include?(mysql_index_type) ? mysql_index_type : nil index_using = INDEX_USINGS.include?(mysql_index_type) ? mysql_index_type : nil - indexes << IndexDefinition.new(row[:Table], row[:Key_name], row[:Non_unique].to_i == 0, [], [], nil, nil, index_type, index_using) + indexes << IndexDefinition.new(row[:Table], row[:Key_name], row[:Non_unique].to_i == 0, [], {}, nil, nil, index_type, index_using, row[:Index_comment].presence) end indexes.last.columns << row[:Column_name] - indexes.last.lengths << row[:Sub_part] + indexes.last.lengths.merge!(row[:Column_name] => row[:Sub_part].to_i) if row[:Sub_part] end end indexes end - # Returns an array of +Column+ objects for the table specified by +table_name+. - def columns(table_name)#:nodoc: - sql = "SHOW FULL FIELDS FROM #{quote_table_name(table_name)}" - execute_and_free(sql, 'SCHEMA') do |result| - each_hash(result).map do |field| - field_name = set_field_encoding(field[:Field]) - sql_type = field[:Type] - type_metadata = fetch_type_metadata(sql_type, field[:Extra]) - new_column(field_name, field[:Default], type_metadata, field[:Null] == "YES", nil, field[:Collation]) - end + def new_column_from_field(table_name, field) # :nodoc: + type_metadata = fetch_type_metadata(field[:Type], field[:Extra]) + if type_metadata.type == :datetime && field[:Default] == "CURRENT_TIMESTAMP" + default, default_function = nil, field[:Default] + else + default, default_function = field[:Default], nil end + new_column(field[:Field], default, type_metadata, field[:Null] == "YES", table_name, default_function, field[:Collation], comment: field[:Comment].presence) + end + + def table_comment(table_name) # :nodoc: + schema, name = extract_schema_qualified_name(table_name) + + select_value(<<-SQL.strip_heredoc, "SCHEMA") + SELECT table_comment + FROM information_schema.tables + WHERE table_schema = #{quote(schema)} + AND table_name = #{quote(name)} + SQL end - def create_table(table_name, options = {}) #:nodoc: - super(table_name, options.reverse_merge(:options => "ENGINE=InnoDB")) + def create_table(table_name, **options) #:nodoc: + super(table_name, options: "ENGINE=InnoDB", **options) end def bulk_change_table(table_name, operations) #:nodoc: @@ -632,7 +470,7 @@ module ActiveRecord def change_column_default(table_name, column_name, default_or_changes) #:nodoc: default = extract_new_default_value(default_or_changes) column = column_for(table_name, column_name) - change_column table_name, column_name, column.sql_type, :default => default + change_column table_name, column_name, column.sql_type, default: default end def change_column_null(table_name, column_name, null, default = nil) #:nodoc: @@ -642,7 +480,7 @@ module ActiveRecord execute("UPDATE #{quote_table_name(table_name)} SET #{quote_column_name(column_name)}=#{quote(default)} WHERE #{quote_column_name(column_name)} IS NULL") end - change_column table_name, column_name, column.sql_type, :null => null + change_column table_name, column_name, column.sql_type, null: null end def change_column(table_name, column_name, type, options = {}) #:nodoc: @@ -655,106 +493,143 @@ module ActiveRecord end def add_index(table_name, column_name, options = {}) #:nodoc: - index_name, index_type, index_columns, _, index_algorithm, index_using = add_index_options(table_name, column_name, options) - execute "CREATE #{index_type} INDEX #{quote_column_name(index_name)} #{index_using} ON #{quote_table_name(table_name)} (#{index_columns}) #{index_algorithm}" + index_name, index_type, index_columns, _, index_algorithm, index_using, comment = add_index_options(table_name, column_name, options) + sql = "CREATE #{index_type} INDEX #{quote_column_name(index_name)} #{index_using} ON #{quote_table_name(table_name)} (#{index_columns}) #{index_algorithm}" + execute add_sql_comment!(sql, comment) + end + + def add_sql_comment!(sql, comment) # :nodoc: + sql << " COMMENT #{quote(comment)}" if comment.present? + sql end def foreign_keys(table_name) - fk_info = select_all <<-SQL.strip_heredoc - SELECT fk.referenced_table_name as 'to_table' - ,fk.referenced_column_name as 'primary_key' - ,fk.column_name as 'column' - ,fk.constraint_name as 'name' + raise ArgumentError unless table_name.present? + + schema, name = extract_schema_qualified_name(table_name) + + fk_info = select_all(<<-SQL.strip_heredoc, "SCHEMA") + SELECT fk.referenced_table_name AS 'to_table', + fk.referenced_column_name AS 'primary_key', + fk.column_name AS 'column', + fk.constraint_name AS 'name', + rc.update_rule AS 'on_update', + rc.delete_rule AS 'on_delete' FROM information_schema.key_column_usage fk - WHERE fk.referenced_column_name is not null - AND fk.table_schema = '#{@config[:database]}' - AND fk.table_name = '#{table_name}' + JOIN information_schema.referential_constraints rc + USING (constraint_schema, constraint_name) + WHERE fk.referenced_column_name IS NOT NULL + AND fk.table_schema = #{quote(schema)} + AND fk.table_name = #{quote(name)} SQL - create_table_info = select_one("SHOW CREATE TABLE #{quote_table_name(table_name)}")["Create Table"] - fk_info.map do |row| options = { - column: row['column'], - name: row['name'], - primary_key: row['primary_key'] + column: row["column"], + name: row["name"], + primary_key: row["primary_key"] } - options[:on_update] = extract_foreign_key_action(create_table_info, row['name'], "UPDATE") - options[:on_delete] = extract_foreign_key_action(create_table_info, row['name'], "DELETE") + options[:on_update] = extract_foreign_key_action(row["on_update"]) + options[:on_delete] = extract_foreign_key_action(row["on_delete"]) - ForeignKeyDefinition.new(table_name, row['to_table'], options) + ForeignKeyDefinition.new(table_name, row["to_table"], options) end end - def table_options(table_name) - create_table_info = select_one("SHOW CREATE TABLE #{quote_table_name(table_name)}")["Create Table"] + def table_options(table_name) # :nodoc: + table_options = {} + + create_table_info = create_table_info(table_name) # strip create_definitions and partition_options - raw_table_options = create_table_info.sub(/\A.*\n\) /m, '').sub(/\n\/\*!.*\*\/\n\z/m, '').strip + raw_table_options = create_table_info.sub(/\A.*\n\) /m, "").sub(/\n\/\*!.*\*\/\n\z/m, "").strip # strip AUTO_INCREMENT - raw_table_options.sub(/(ENGINE=\w+)(?: AUTO_INCREMENT=\d+)/, '\1') - end + raw_table_options.sub!(/(ENGINE=\w+)(?: AUTO_INCREMENT=\d+)/, '\1') - # 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' - binary_to_sql(limit) - when 'integer' - integer_to_sql(limit) - when 'text' - text_to_sql(limit) - else - super + table_options[:options] = raw_table_options + + # strip COMMENT + if raw_table_options.sub!(/ COMMENT='.+'/, "") + table_options[:comment] = table_comment(table_name) end - end - # SHOW VARIABLES LIKE 'name' - def show_variable(name) - variables = select_all("SHOW VARIABLES LIKE '#{name}'", 'SCHEMA') - variables.first['Value'] unless variables.empty? + table_options end - # Returns a table's primary key and belonging sequence. - def pk_and_sequence_for(table) - execute_and_free("SHOW CREATE TABLE #{quote_table_name(table)}", 'SCHEMA') do |result| - create_table = each_hash(result).first[:"Create Table"] - if create_table.to_s =~ /PRIMARY KEY\s+(?:USING\s+\w+\s+)?\((.+)\)/ - keys = $1.split(",").map { |key| key.delete('`"') } - keys.length == 1 ? [keys.first, nil] : nil + # Maps logical Rails types to MySQL-specific data types. + def type_to_sql(type, limit = nil, precision = nil, scale = nil, unsigned = nil) + sql = \ + case type.to_s + when "integer" + integer_to_sql(limit) + when "text" + text_to_sql(limit) + when "blob" + binary_to_sql(limit) + when "binary" + if (0..0xfff) === limit + "varbinary(#{limit})" + else + binary_to_sql(limit) + end else - nil + super(type, limit, precision, scale) end - end + + sql << " unsigned" if unsigned && type != :primary_key + sql end - # Returns just a table's primary key - def primary_key(table) - pk_and_sequence = pk_and_sequence_for(table) - pk_and_sequence && pk_and_sequence.first + # SHOW VARIABLES LIKE 'name' + def show_variable(name) + select_value("SELECT @@#{name}", "SCHEMA") + rescue ActiveRecord::StatementInvalid + nil end - def case_sensitive_modifier(node, table_attribute) - node = Arel::Nodes.build_quoted node, table_attribute - Arel::Nodes::Bin.new(node) + def primary_keys(table_name) # :nodoc: + raise ArgumentError unless table_name.present? + + schema, name = extract_schema_qualified_name(table_name) + + select_values(<<-SQL.strip_heredoc, "SCHEMA") + SELECT column_name + FROM information_schema.key_column_usage + WHERE constraint_name = 'PRIMARY' + AND table_schema = #{quote(schema)} + AND table_name = #{quote(name)} + ORDER BY ordinal_position + SQL end def case_sensitive_comparison(table, attribute, column, value) - if column.case_sensitive? - table[attribute].eq(value) + if column.collation && !column.case_sensitive? + table[attribute].eq(Arel::Nodes::Bin.new(Arel::Nodes::BindParam.new)) else super end end - def case_insensitive_comparison(table, attribute, column, value) - if column.case_sensitive? - super - else - table[attribute].eq(value) - end + def can_perform_case_insensitive_comparison_for?(column) + column.case_sensitive? + end + private :can_perform_case_insensitive_comparison_for? + + # In MySQL 5.7.5 and up, ONLY_FULL_GROUP_BY affects handling of queries that use + # DISTINCT and ORDER BY. It requires the ORDER BY columns in the select list for + # distinct queries, and requires that the ORDER BY include the distinct column. + # See https://dev.mysql.com/doc/refman/5.7/en/group-by-handling.html + def columns_for_distinct(columns, orders) # :nodoc: + order_columns = orders.reject(&:blank?).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)\b/i, "") + }.reject(&:blank?).map.with_index { |column, i| "#{column} AS alias_#{i}" } + + [super, *order_columns].join(", ") end def strict_mode? @@ -765,298 +640,367 @@ module ActiveRecord !native_database_types[type].nil? end - protected + private - def initialize_type_map(m) # :nodoc: - super + def initialize_type_map(m) + super - register_class_with_limit m, %r(char)i, MysqlString + register_class_with_limit m, %r(char)i, MysqlString + + m.register_type %r(tinytext)i, Type::Text.new(limit: 2**8 - 1) + m.register_type %r(tinyblob)i, Type::Binary.new(limit: 2**8 - 1) + m.register_type %r(text)i, Type::Text.new(limit: 2**16 - 1) + m.register_type %r(blob)i, Type::Binary.new(limit: 2**16 - 1) + m.register_type %r(mediumtext)i, Type::Text.new(limit: 2**24 - 1) + m.register_type %r(mediumblob)i, Type::Binary.new(limit: 2**24 - 1) + m.register_type %r(longtext)i, Type::Text.new(limit: 2**32 - 1) + m.register_type %r(longblob)i, Type::Binary.new(limit: 2**32 - 1) + m.register_type %r(^float)i, Type::Float.new(limit: 24) + m.register_type %r(^double)i, Type::Float.new(limit: 53) + m.register_type %r(^json)i, MysqlJson.new + + register_integer_type m, %r(^bigint)i, limit: 8 + register_integer_type m, %r(^int)i, limit: 4 + register_integer_type m, %r(^mediumint)i, limit: 3 + register_integer_type m, %r(^smallint)i, limit: 2 + register_integer_type m, %r(^tinyint)i, limit: 1 + + m.register_type %r(^tinyint\(1\))i, Type::Boolean.new if emulate_booleans + m.alias_type %r(year)i, "integer" + m.alias_type %r(bit)i, "binary" + + m.register_type(%r(enum)i) do |sql_type| + limit = sql_type[/^enum\((.+)\)/i, 1] + .split(",").map { |enum| enum.strip.length - 2 }.max + MysqlString.new(limit: limit) + end - m.register_type %r(tinytext)i, Type::Text.new(limit: 2**8 - 1) - m.register_type %r(tinyblob)i, Type::Binary.new(limit: 2**8 - 1) - m.register_type %r(text)i, Type::Text.new(limit: 2**16 - 1) - m.register_type %r(blob)i, Type::Binary.new(limit: 2**16 - 1) - m.register_type %r(mediumtext)i, Type::Text.new(limit: 2**24 - 1) - m.register_type %r(mediumblob)i, Type::Binary.new(limit: 2**24 - 1) - m.register_type %r(longtext)i, Type::Text.new(limit: 2**32 - 1) - m.register_type %r(longblob)i, Type::Binary.new(limit: 2**32 - 1) - m.register_type %r(^float)i, Type::Float.new(limit: 24) - m.register_type %r(^double)i, Type::Float.new(limit: 53) + m.register_type(%r(^set)i) do |sql_type| + limit = sql_type[/^set\((.+)\)/i, 1] + .split(",").map { |set| set.strip.length - 1 }.sum - 1 + MysqlString.new(limit: limit) + end + end - register_integer_type m, %r(^bigint)i, limit: 8 - register_integer_type m, %r(^int)i, limit: 4 - register_integer_type m, %r(^mediumint)i, limit: 3 - register_integer_type m, %r(^smallint)i, limit: 2 - register_integer_type m, %r(^tinyint)i, limit: 1 + def register_integer_type(mapping, key, options) + mapping.register_type(key) do |sql_type| + if /\bunsigned\b/.match?(sql_type) + Type::UnsignedInteger.new(options) + else + Type::Integer.new(options) + end + end + end - m.alias_type %r(tinyint\(1\))i, 'boolean' if emulate_booleans - m.alias_type %r(set)i, 'varchar' - m.alias_type %r(year)i, 'integer' - m.alias_type %r(bit)i, 'binary' + def extract_precision(sql_type) + if /time/.match?(sql_type) + super || 0 + else + super + end + end - m.register_type(%r(enum)i) do |sql_type| - limit = sql_type[/^enum\((.+)\)/i, 1] - .split(',').map{|enum| enum.strip.length - 2}.max - MysqlString.new(limit: limit) + def fetch_type_metadata(sql_type, extra = "") + MySQL::TypeMetadata.new(super(sql_type), extra: extra) end - end - def register_integer_type(mapping, key, options) # :nodoc: - mapping.register_type(key) do |sql_type| - if /unsigned/i =~ sql_type - Type::UnsignedInteger.new(options) - else - Type::Integer.new(options) + def add_index_length(quoted_columns, **options) + if length = options[:length] + case length + when Hash + length = length.symbolize_keys + quoted_columns.each { |name, column| column << "(#{length[name]})" if length[name].present? } + when Integer + quoted_columns.each { |name, column| column << "(#{length})" } + end end + + quoted_columns end - end - def extract_precision(sql_type) - if /time/ === sql_type - super || 0 - else + def add_options_for_index_columns(quoted_columns, **options) + quoted_columns = add_index_length(quoted_columns, options) super end - end - def fetch_type_metadata(sql_type, extra = "") - MysqlTypeMetadata.new(super(sql_type), extra: extra, strict: strict_mode?) - end - - def add_index_length(option_strings, column_names, options = {}) - if options.is_a?(Hash) && length = options[:length] - case length - when Hash - column_names.each {|name| option_strings[name] += "(#{length[name]})" if length.has_key?(name) && length[name].present?} - when Fixnum - column_names.each {|name| option_strings[name] += "(#{length})"} + # See https://dev.mysql.com/doc/refman/5.7/en/error-messages-server.html + ER_DUP_ENTRY = 1062 + ER_NOT_NULL_VIOLATION = 1048 + ER_DO_NOT_HAVE_DEFAULT = 1364 + ER_NO_REFERENCED_ROW_2 = 1452 + ER_DATA_TOO_LONG = 1406 + ER_OUT_OF_RANGE = 1264 + ER_LOCK_DEADLOCK = 1213 + ER_CANNOT_ADD_FOREIGN = 1215 + ER_CANNOT_CREATE_TABLE = 1005 + + def translate_exception(exception, message) + case error_number(exception) + when ER_DUP_ENTRY + RecordNotUnique.new(message) + when ER_NO_REFERENCED_ROW_2 + InvalidForeignKey.new(message) + when ER_CANNOT_ADD_FOREIGN + mismatched_foreign_key(message) + when ER_CANNOT_CREATE_TABLE + if message.include?("errno: 150") + mismatched_foreign_key(message) + else + super + end + when ER_DATA_TOO_LONG + ValueTooLong.new(message) + when ER_OUT_OF_RANGE + RangeError.new(message) + when ER_NOT_NULL_VIOLATION, ER_DO_NOT_HAVE_DEFAULT + NotNullViolation.new(message) + when ER_LOCK_DEADLOCK + Deadlocked.new(message) + else + super end end - return option_strings - end + def add_column_sql(table_name, column_name, type, options = {}) + td = create_table_definition(table_name) + cd = td.new_column_definition(column_name, type, options) + schema_creation.accept(AddColumnDefinition.new(cd)) + end - def quoted_columns_for_index(column_names, options = {}) - option_strings = Hash[column_names.map {|name| [name, '']}] + def change_column_sql(table_name, column_name, type, options = {}) + column = column_for(table_name, column_name) - # add index length - option_strings = add_index_length(option_strings, column_names, options) + unless options_include_default?(options) + options[:default] = column.default + end - # add index sort order - option_strings = add_index_sort_order(option_strings, column_names, options) + unless options.has_key?(:null) + options[:null] = column.null + end - column_names.map {|name| quote_column_name(name) + option_strings[name]} - end + unless options.key?(:comment) + options[:comment] = column.comment + end - def translate_exception(exception, message) - case error_number(exception) - when 1062 - RecordNotUnique.new(message, exception) - when 1452 - InvalidForeignKey.new(message, exception) - else - super + td = create_table_definition(table_name) + cd = td.new_column_definition(column.name, type, options) + schema_creation.accept(ChangeColumnDefinition.new(cd, column.name)) end - end - def add_column_sql(table_name, column_name, type, options = {}) - td = create_table_definition(table_name) - cd = td.new_column_definition(column_name, type, options) - schema_creation.accept(AddColumnDefinition.new(cd)) - end - - def change_column_sql(table_name, column_name, type, options = {}) - column = column_for(table_name, column_name) + def rename_column_sql(table_name, column_name, new_column_name) + column = column_for(table_name, column_name) + options = { + default: column.default, + null: column.null, + auto_increment: column.auto_increment? + } - unless options_include_default?(options) - options[:default] = column.default + current_type = select_one("SHOW COLUMNS FROM #{quote_table_name(table_name)} LIKE '#{column_name}'", "SCHEMA")["Type"] + td = create_table_definition(table_name) + cd = td.new_column_definition(new_column_name, current_type, options) + schema_creation.accept(ChangeColumnDefinition.new(cd, column.name)) end - unless options.has_key?(:null) - options[:null] = column.null + def remove_column_sql(table_name, column_name, type = nil, options = {}) + "DROP #{quote_column_name(column_name)}" end - td = create_table_definition(table_name) - cd = td.new_column_definition(column.name, type, options) - schema_creation.accept(ChangeColumnDefinition.new(cd, column.name)) - end - - def rename_column_sql(table_name, column_name, new_column_name) - column = column_for(table_name, column_name) - options = { - default: column.default, - null: column.null, - auto_increment: column.auto_increment? - } - - current_type = select_one("SHOW COLUMNS FROM #{quote_table_name(table_name)} LIKE '#{column_name}'", 'SCHEMA')["Type"] - td = create_table_definition(table_name) - cd = td.new_column_definition(new_column_name, current_type, options) - schema_creation.accept(ChangeColumnDefinition.new(cd, column.name)) - end - - def remove_column_sql(table_name, column_name, type = nil, options = {}) - "DROP #{quote_column_name(column_name)}" - end - - def remove_columns_sql(table_name, *column_names) - column_names.map {|column_name| remove_column_sql(table_name, column_name) } - end + def remove_columns_sql(table_name, *column_names) + column_names.map { |column_name| remove_column_sql(table_name, column_name) } + end - def add_index_sql(table_name, column_name, options = {}) - index_name, index_type, index_columns, _, index_algorithm, index_using = add_index_options(table_name, column_name, options) - index_algorithm[0, 0] = ", " if index_algorithm.present? - "ADD #{index_type} INDEX #{quote_column_name(index_name)} #{index_using} (#{index_columns})#{index_algorithm}" - end + def add_index_sql(table_name, column_name, options = {}) + index_name, index_type, index_columns, _, index_algorithm, index_using = add_index_options(table_name, column_name, options) + index_algorithm[0, 0] = ", " if index_algorithm.present? + "ADD #{index_type} INDEX #{quote_column_name(index_name)} #{index_using} (#{index_columns})#{index_algorithm}" + end - def remove_index_sql(table_name, options = {}) - index_name = index_name_for_remove(table_name, options) - "DROP INDEX #{index_name}" - end + def remove_index_sql(table_name, options = {}) + index_name = index_name_for_remove(table_name, options) + "DROP INDEX #{index_name}" + end - def add_timestamps_sql(table_name, options = {}) - [add_column_sql(table_name, :created_at, :datetime, options), add_column_sql(table_name, :updated_at, :datetime, options)] - end + def add_timestamps_sql(table_name, options = {}) + [add_column_sql(table_name, :created_at, :datetime, options), add_column_sql(table_name, :updated_at, :datetime, options)] + end - def remove_timestamps_sql(table_name, options = {}) - [remove_column_sql(table_name, :updated_at), remove_column_sql(table_name, :created_at)] - end + def remove_timestamps_sql(table_name, options = {}) + [remove_column_sql(table_name, :updated_at), remove_column_sql(table_name, :created_at)] + end - private + # MySQL is too stupid to create a temporary table for use subquery, so we have + # to give it some prompting in the form of a subsubquery. Ugh! + def subquery_for(key, select) + subsubselect = select.clone + subsubselect.projections = [key] - # MySQL is too stupid to create a temporary table for use subquery, so we have - # to give it some prompting in the form of a subsubquery. Ugh! - def subquery_for(key, select) - subsubselect = select.clone - subsubselect.projections = [key] + # Materialize subquery by adding distinct + # to work with MySQL 5.7.6 which sets optimizer_switch='derived_merge=on' + subsubselect.distinct unless select.limit || select.offset || select.orders.any? - subselect = Arel::SelectManager.new(select.engine) - subselect.project Arel.sql(key.name) - # Materialized subquery by adding distinct - # to work with MySQL 5.7.6 which sets optimizer_switch='derived_merge=on' - subselect.from subsubselect.distinct.as('__active_record_temp') - end + subselect = Arel::SelectManager.new(select.engine) + subselect.project Arel.sql(key.name) + subselect.from subsubselect.as("__active_record_temp") + end - def version - @version ||= full_version.scan(/^(\d+)\.(\d+)\.(\d+)/).flatten.map(&:to_i) - end + def supports_rename_index? + mariadb? ? false : version >= "5.7.6" + end - def mariadb? - full_version =~ /mariadb/i - end + def configure_connection + variables = @config.fetch(:variables, {}).stringify_keys - def supports_rename_index? - mariadb? ? false : (version[0] == 5 && version[1] >= 7) || version[0] >= 6 - end + # By default, MySQL 'where id is null' selects the last inserted id; Turn this off. + variables["sql_auto_is_null"] = 0 - def configure_connection - variables = @config.fetch(:variables, {}).stringify_keys + # Increase timeout so the server doesn't disconnect us. + wait_timeout = @config[:wait_timeout] + wait_timeout = 2147483 unless wait_timeout.is_a?(Integer) + variables["wait_timeout"] = self.class.type_cast_config_to_integer(wait_timeout) - # By default, MySQL 'where id is null' selects the last inserted id; Turn this off. - variables['sql_auto_is_null'] = 0 + defaults = [":default", :default].to_set - # Increase timeout so the server doesn't disconnect us. - wait_timeout = @config[:wait_timeout] - wait_timeout = 2147483 unless wait_timeout.is_a?(Fixnum) - variables['wait_timeout'] = self.class.type_cast_config_to_integer(wait_timeout) + # Make MySQL reject illegal values rather than truncating or blanking them, see + # http://dev.mysql.com/doc/refman/5.7/en/sql-mode.html#sqlmode_strict_all_tables + # If the user has provided another value for sql_mode, don't replace it. + if sql_mode = variables.delete("sql_mode") + sql_mode = quote(sql_mode) + elsif !defaults.include?(strict_mode?) + if strict_mode? + sql_mode = "CONCAT(@@sql_mode, ',STRICT_ALL_TABLES')" + else + sql_mode = "REPLACE(@@sql_mode, 'STRICT_TRANS_TABLES', '')" + sql_mode = "REPLACE(#{sql_mode}, 'STRICT_ALL_TABLES', '')" + sql_mode = "REPLACE(#{sql_mode}, 'TRADITIONAL', '')" + end + sql_mode = "CONCAT(#{sql_mode}, ',NO_AUTO_VALUE_ON_ZERO')" + end + sql_mode_assignment = "@@SESSION.sql_mode = #{sql_mode}, " if sql_mode + + # NAMES does not have an equals sign, see + # http://dev.mysql.com/doc/refman/5.7/en/set-statement.html#id944430 + # (trailing comma because variable_assignments will always have content) + if @config[:encoding] + encoding = "NAMES #{@config[:encoding]}" + encoding << " COLLATE #{@config[:collation]}" if @config[:collation] + encoding << ", " + end - defaults = [':default', :default].to_set + # Gather up all of the SET variables... + variable_assignments = variables.map do |k, v| + if defaults.include?(v) + "@@SESSION.#{k} = DEFAULT" # Sets the value to the global or compile default + elsif !v.nil? + "@@SESSION.#{k} = #{quote(v)}" + end + # or else nil; compact to clear nils out + end.compact.join(", ") - # Make MySQL reject illegal values rather than truncating or blanking them, see - # http://dev.mysql.com/doc/refman/5.6/en/sql-mode.html#sqlmode_strict_all_tables - # If the user has provided another value for sql_mode, don't replace it. - unless variables.has_key?('sql_mode') || defaults.include?(@config[:strict]) - variables['sql_mode'] = strict_mode? ? 'STRICT_ALL_TABLES' : '' + # ...and send them all in one query + execute "SET #{encoding} #{sql_mode_assignment} #{variable_assignments}" end - # NAMES does not have an equals sign, see - # http://dev.mysql.com/doc/refman/5.6/en/set-statement.html#id944430 - # (trailing comma because variable_assignments will always have content) - if @config[:encoding] - encoding = "NAMES #{@config[:encoding]}" - encoding << " COLLATE #{@config[:collation]}" if @config[:collation] - encoding << ", " + def column_definitions(table_name) # :nodoc: + execute_and_free("SHOW FULL FIELDS FROM #{quote_table_name(table_name)}", "SCHEMA") do |result| + each_hash(result) + end end - # Gather up all of the SET variables... - variable_assignments = variables.map do |k, v| - if defaults.include?(v) - "@@SESSION.#{k} = DEFAULT" # Sets the value to the global or compile default - elsif !v.nil? - "@@SESSION.#{k} = #{quote(v)}" + def extract_foreign_key_action(specifier) # :nodoc: + case specifier + when "CASCADE"; :cascade + when "SET NULL"; :nullify end - # or else nil; compact to clear nils out - end.compact.join(', ') + end - # ...and send them all in one query - @connection.query "SET #{encoding} #{variable_assignments}" - end + def create_table_info(table_name) # :nodoc: + select_one("SHOW CREATE TABLE #{quote_table_name(table_name)}")["Create Table"] + end - def extract_foreign_key_action(structure, name, action) # :nodoc: - if structure =~ /CONSTRAINT #{quote_column_name(name)} FOREIGN KEY .* REFERENCES .* ON #{action} (CASCADE|SET NULL|RESTRICT)/ - case $1 - when 'CASCADE'; :cascade - when 'SET NULL'; :nullify - end + def create_table_definition(*args) # :nodoc: + MySQL::TableDefinition.new(*args) end - end - def create_table_definition(name, temporary = false, options = nil, as = nil) # :nodoc: - TableDefinition.new(native_database_types, name, temporary, options, as) - end + def mismatched_foreign_key(message) + parts = message.scan(/`(\w+)`[ $)]/).flatten + MismatchedForeignKey.new( + self, + message: message, + table: parts[0], + foreign_key: parts[1], + target_table: parts[2], + primary_key: parts[3], + ) + end - def binary_to_sql(limit) # :nodoc: - case limit - when 0..0xfff; "varbinary(#{limit})" - when nil; "blob" - when 0x1000..0xffffffff; "blob(#{limit})" - else raise(ActiveRecordError, "No binary type has character length #{limit}") + def extract_schema_qualified_name(string) # :nodoc: + schema, name = string.to_s.scan(/[^`.\s]+|`[^`]*`/) + schema, name = @config[:database], schema unless name + [schema, name] end - end - def integer_to_sql(limit) # :nodoc: - case limit - when 1; 'tinyint' - when 2; 'smallint' - when 3; 'mediumint' - when nil, 4, 11; 'int(11)' # compatibility with MySQL default - when 5..8; 'bigint' - else raise(ActiveRecordError, "No integer type has byte size #{limit}") + def integer_to_sql(limit) # :nodoc: + case limit + when 1; "tinyint" + when 2; "smallint" + when 3; "mediumint" + when nil, 4; "int" + when 5..8; "bigint" + else raise(ActiveRecordError, "No integer type has byte size #{limit}. Use a decimal with scale 0 instead.") + end end - end - def text_to_sql(limit) # :nodoc: - case limit - when 0..0xff; 'tinytext' - when nil, 0x100..0xffff; 'text' - when 0x10000..0xffffff; 'mediumtext' - when 0x1000000..0xffffffff; 'longtext' - else raise(ActiveRecordError, "No text type has character length #{limit}") + def text_to_sql(limit) # :nodoc: + case limit + when 0..0xff; "tinytext" + when nil, 0x100..0xffff; "text" + when 0x10000..0xffffff; "mediumtext" + when 0x1000000..0xffffffff; "longtext" + else raise(ActiveRecordError, "No text type has byte length #{limit}") + end end - end - class MysqlString < Type::String # :nodoc: - def serialize(value) - case value - when true then "1" - when false then "0" - else super + def binary_to_sql(limit) # :nodoc: + case limit + when 0..0xff; "tinyblob" + when nil, 0x100..0xffff; "blob" + when 0x10000..0xffffff; "mediumblob" + when 0x1000000..0xffffffff; "longblob" + else raise(ActiveRecordError, "No binary type has byte length #{limit}") end end - private + class MysqlJson < Type::Internal::AbstractJson # :nodoc: + def changed_in_place?(raw_old_value, new_value) + # Normalization is required because MySQL JSON data format includes + # the space between the elements. + super(serialize(deserialize(raw_old_value)), new_value) + end + end - def cast_value(value) - case value - when true then "1" - when false then "0" - else super + class MysqlString < Type::String # :nodoc: + def serialize(value) + case value + when true then MySQL::Quoting::QUOTED_TRUE + when false then MySQL::Quoting::QUOTED_FALSE + else super + end end + + private + + def cast_value(value) + case value + when true then MySQL::Quoting::QUOTED_TRUE + when false then MySQL::Quoting::QUOTED_FALSE + else super + end + end end - end - ActiveRecord::Type.register(:string, MysqlString, adapter: :mysql) - ActiveRecord::Type.register(:string, MysqlString, adapter: :mysql2) + ActiveRecord::Type.register(:json, MysqlJson, adapter: :mysql2) + ActiveRecord::Type.register(:string, MysqlString, adapter: :mysql2) + ActiveRecord::Type.register(:unsigned_integer, Type::UnsignedInteger, adapter: :mysql2) end end end diff --git a/activerecord/lib/active_record/connection_adapters/column.rb b/activerecord/lib/active_record/connection_adapters/column.rb index 4b95b0681d..61cd7ae4cc 100644 --- a/activerecord/lib/active_record/connection_adapters/column.rb +++ b/activerecord/lib/active_record/connection_adapters/column.rb @@ -1,35 +1,27 @@ -require 'set' - module ActiveRecord # :stopdoc: module ConnectionAdapters # An abstract definition of a column in a table. class Column - FALSE_VALUES = [false, 0, '0', 'f', 'F', 'false', 'FALSE', 'off', 'OFF'].to_set - - module Format - ISO_DATE = /\A(\d{4})-(\d\d)-(\d\d)\z/ - ISO_DATETIME = /\A(\d{4})-(\d\d)-(\d\d) (\d\d):(\d\d):(\d\d)(\.\d+)?\z/ - end - - attr_reader :name, :null, :sql_type_metadata, :default, :default_function, :collation + attr_reader :name, :default, :sql_type_metadata, :null, :table_name, :default_function, :collation, :comment delegate :precision, :scale, :limit, :type, :sql_type, to: :sql_type_metadata, allow_nil: true # Instantiates a new column in the table. # - # +name+ is the column's name, such as <tt>supplier_id</tt> in <tt>supplier_id int(11)</tt>. + # +name+ is the column's name, such as <tt>supplier_id</tt> in <tt>supplier_id int</tt>. # +default+ is the type-casted default value, such as +new+ in <tt>sales_stage varchar(20) default 'new'</tt>. # +sql_type_metadata+ is various information about the type of the column # +null+ determines if this column allows +NULL+ values. - def initialize(name, default, sql_type_metadata = nil, null = true, default_function = nil, collation = nil) - @name = name + def initialize(name, default, sql_type_metadata = nil, null = true, table_name = nil, default_function = nil, collation = nil, comment: nil) + @name = name.freeze + @table_name = table_name @sql_type_metadata = sql_type_metadata @null = null @default = default @default_function = default_function @collation = collation - @table_name = nil + @comment = comment end def has_default? @@ -37,7 +29,7 @@ module ActiveRecord end def bigint? - /bigint/ === sql_type + /\Abigint\b/.match?(sql_type) end # Returns the human name of the column name. @@ -48,6 +40,28 @@ module ActiveRecord Base.human_attribute_name(@name) end + def init_with(coder) + @name = coder["name"] + @table_name = coder["table_name"] + @sql_type_metadata = coder["sql_type_metadata"] + @null = coder["null"] + @default = coder["default"] + @default_function = coder["default_function"] + @collation = coder["collation"] + @comment = coder["comment"] + end + + def encode_with(coder) + coder["name"] = @name + coder["table_name"] = @table_name + coder["sql_type_metadata"] = @sql_type_metadata + coder["null"] = @null + coder["default"] = @default + coder["default_function"] = @default_function + coder["collation"] = @collation + coder["comment"] = @comment + end + def ==(other) other.is_a?(Column) && attributes_for_hash == other.attributes_for_hash @@ -60,9 +74,9 @@ module ActiveRecord protected - def attributes_for_hash - [self.class, name, default, sql_type_metadata, null, default_function, collation] - end + def attributes_for_hash + [self.class, name, default, sql_type_metadata, null, table_name, default_function, collation] + end end class NullColumn < Column diff --git a/activerecord/lib/active_record/connection_adapters/connection_specification.rb b/activerecord/lib/active_record/connection_adapters/connection_specification.rb index 08d46fca96..dcf56997db 100644 --- a/activerecord/lib/active_record/connection_adapters/connection_specification.rb +++ b/activerecord/lib/active_record/connection_adapters/connection_specification.rb @@ -1,21 +1,24 @@ -require 'uri' +require "uri" module ActiveRecord module ConnectionAdapters class ConnectionSpecification #:nodoc: - attr_reader :config, :adapter_method + attr_reader :name, :config, :adapter_method - def initialize(config, adapter_method) - @config, @adapter_method = config, adapter_method + def initialize(name, config, adapter_method) + @name, @config, @adapter_method = name, config, adapter_method end def initialize_dup(original) @config = original.config.dup end + def to_hash + @config.merge(name: @name) + end + # Expands a connection string into a hash. class ConnectionUrlResolver # :nodoc: - # == Example # # url = "postgresql://foo:bar@localhost:9000/foo_test?pool=5&timeout=3000" @@ -33,11 +36,11 @@ module ActiveRecord def initialize(url) raise "Database URL cannot be empty" if url.blank? @uri = uri_parser.parse(url) - @adapter = @uri.scheme.tr('-', '_') + @adapter = @uri.scheme && @uri.scheme.tr("-", "_") @adapter = "postgresql" if @adapter == "postgres" if @uri.opaque - @uri.opaque, @query = @uri.opaque.split('?', 2) + @uri.opaque, @query = @uri.opaque.split("?", 2) else @query = @uri.query end @@ -45,65 +48,65 @@ module ActiveRecord # Converts the given URL to a full connection hash. def to_hash - config = raw_config.reject { |_,value| value.blank? } - config.map { |key,value| config[key] = uri_parser.unescape(value) if value.is_a? String } + config = raw_config.reject { |_, value| value.blank? } + config.map { |key, value| config[key] = uri_parser.unescape(value) if value.is_a? String } config end private - def uri - @uri - end + def uri + @uri + end - def uri_parser - @uri_parser ||= URI::Parser.new - end + def uri_parser + @uri_parser ||= URI::Parser.new + end - # Converts the query parameters of the URI into a hash. - # - # "localhost?pool=5&reaping_frequency=2" - # # => { "pool" => "5", "reaping_frequency" => "2" } - # - # returns empty hash if no query present. - # - # "localhost" - # # => {} - def query_hash - Hash[(@query || '').split("&").map { |pair| pair.split("=") }] - end + # Converts the query parameters of the URI into a hash. + # + # "localhost?pool=5&reaping_frequency=2" + # # => { "pool" => "5", "reaping_frequency" => "2" } + # + # returns empty hash if no query present. + # + # "localhost" + # # => {} + def query_hash + Hash[(@query || "").split("&").map { |pair| pair.split("=") }] + end - def raw_config - if uri.opaque - query_hash.merge({ - "adapter" => @adapter, - "database" => uri.opaque }) - else - query_hash.merge({ - "adapter" => @adapter, - "username" => uri.user, - "password" => uri.password, - "port" => uri.port, - "database" => database_from_path, - "host" => uri.hostname }) + def raw_config + if uri.opaque + query_hash.merge( + "adapter" => @adapter, + "database" => uri.opaque) + else + query_hash.merge( + "adapter" => @adapter, + "username" => uri.user, + "password" => uri.password, + "port" => uri.port, + "database" => database_from_path, + "host" => uri.hostname) + end end - end - # Returns name of the database. - def database_from_path - if @adapter == 'sqlite3' - # 'sqlite3:/foo' is absolute, because that makes sense. The - # corresponding relative version, 'sqlite3:foo', is handled - # elsewhere, as an "opaque". + # Returns name of the database. + def database_from_path + if @adapter == "sqlite3" + # 'sqlite3:/foo' is absolute, because that makes sense. The + # corresponding relative version, 'sqlite3:foo', is handled + # elsewhere, as an "opaque". - uri.path - else - # Only SQLite uses a filename as the "database" name; for - # anything else, a leading slash would be silly. + uri.path + else + # Only SQLite uses a filename as the "database" name; for + # anything else, a leading slash would be silly. - uri.path.sub(%r{^/}, "") + uri.path.sub(%r{^/}, "") + end end - end end ## @@ -175,81 +178,86 @@ module ActiveRecord rescue Gem::LoadError => e raise Gem::LoadError, "Specified '#{spec[:adapter]}' for database adapter, but the gem is not loaded. Add `gem '#{e.name}'` to your Gemfile (and ensure its version is at the minimum required by ActiveRecord)." rescue LoadError => e - raise LoadError, "Could not load '#{path_to_adapter}'. Make sure that the adapter in config/database.yml is valid. If you use an adapter other than 'mysql', 'mysql2', 'postgresql' or 'sqlite3' add the necessary adapter gem to the Gemfile.", e.backtrace + raise LoadError, "Could not load '#{path_to_adapter}'. Make sure that the adapter in config/database.yml is valid. If you use an adapter other than 'mysql2', 'postgresql' or 'sqlite3' add the necessary adapter gem to the Gemfile.", e.backtrace end adapter_method = "#{spec[:adapter]}_connection" - ConnectionSpecification.new(spec, adapter_method) + + unless ActiveRecord::Base.respond_to?(adapter_method) + raise AdapterNotFound, "database configuration specifies nonexistent #{spec.config[:adapter]} adapter" + end + + ConnectionSpecification.new(spec.delete(:name) || "primary", spec, adapter_method) end private - # Returns fully resolved connection, accepts hash, string or symbol. - # Always returns a hash. - # - # == Examples - # - # Symbol representing current environment. - # - # Resolver.new("production" => {}).resolve_connection(:production) - # # => {} - # - # One layer deep hash of connection values. - # - # Resolver.new({}).resolve_connection("adapter" => "sqlite3") - # # => { "adapter" => "sqlite3" } - # - # Connection URL. - # - # Resolver.new({}).resolve_connection("postgresql://localhost/foo") - # # => { "host" => "localhost", "database" => "foo", "adapter" => "postgresql" } - # - def resolve_connection(spec) - case spec - when Symbol - resolve_symbol_connection spec - when String - resolve_url_connection spec - when Hash - resolve_hash_connection spec + # Returns fully resolved connection, accepts hash, string or symbol. + # Always returns a hash. + # + # == Examples + # + # Symbol representing current environment. + # + # Resolver.new("production" => {}).resolve_connection(:production) + # # => {} + # + # One layer deep hash of connection values. + # + # Resolver.new({}).resolve_connection("adapter" => "sqlite3") + # # => { "adapter" => "sqlite3" } + # + # Connection URL. + # + # Resolver.new({}).resolve_connection("postgresql://localhost/foo") + # # => { "host" => "localhost", "database" => "foo", "adapter" => "postgresql" } + # + def resolve_connection(spec) + case spec + when Symbol + resolve_symbol_connection spec + when String + resolve_url_connection spec + when Hash + resolve_hash_connection spec + end end - end - # Takes the environment such as +:production+ or +:development+. - # This requires that the @configurations was initialized with a key that - # matches. - # - # Resolver.new("production" => {}).resolve_symbol_connection(:production) - # # => {} - # - def resolve_symbol_connection(spec) - if config = configurations[spec.to_s] - resolve_connection(config) - else - raise(AdapterNotSpecified, "'#{spec}' database is not configured. Available: #{configurations.keys.inspect}") + # Takes the environment such as +:production+ or +:development+. + # This requires that the @configurations was initialized with a key that + # matches. + # + # Resolver.new("production" => {}).resolve_symbol_connection(:production) + # # => {} + # + def resolve_symbol_connection(spec) + if config = configurations[spec.to_s] + resolve_connection(config).merge("name" => spec.to_s) + else + raise(AdapterNotSpecified, "'#{spec}' database is not configured. Available: #{configurations.keys.inspect}") + end end - end - # Accepts a hash. Expands the "url" key that contains a - # URL database connection to a full connection - # hash and merges with the rest of the hash. - # Connection details inside of the "url" key win any merge conflicts - def resolve_hash_connection(spec) - if spec["url"] && spec["url"] !~ /^jdbc:/ - connection_hash = resolve_url_connection(spec.delete("url")) - spec.merge!(connection_hash) + # Accepts a hash. Expands the "url" key that contains a + # URL database connection to a full connection + # hash and merges with the rest of the hash. + # Connection details inside of the "url" key win any merge conflicts + def resolve_hash_connection(spec) + if spec["url"] && spec["url"] !~ /^jdbc:/ + connection_hash = resolve_url_connection(spec.delete("url")) + spec.merge!(connection_hash) + end + spec end - spec - end - # Takes a connection URL. - # - # Resolver.new({}).resolve_url_connection("postgresql://localhost/foo") - # # => { "host" => "localhost", "database" => "foo", "adapter" => "postgresql" } - # - def resolve_url_connection(url) - ConnectionUrlResolver.new(url).to_hash - end + # Takes a connection URL. + # + # Resolver.new({}).resolve_url_connection("postgresql://localhost/foo") + # # => { "host" => "localhost", "database" => "foo", "adapter" => "postgresql" } + # + def resolve_url_connection(url) + ConnectionUrlResolver.new(url).to_hash + end end end end diff --git a/activerecord/lib/active_record/connection_adapters/determine_if_preparable_visitor.rb b/activerecord/lib/active_record/connection_adapters/determine_if_preparable_visitor.rb new file mode 100644 index 0000000000..0fdc185c45 --- /dev/null +++ b/activerecord/lib/active_record/connection_adapters/determine_if_preparable_visitor.rb @@ -0,0 +1,22 @@ +module ActiveRecord + module ConnectionAdapters + module DetermineIfPreparableVisitor + attr_reader :preparable + + def accept(*) + @preparable = true + super + end + + def visit_Arel_Nodes_In(*) + @preparable = false + super + end + + def visit_Arel_Nodes_SqlLiteral(*) + @preparable = false + super + end + end + end +end diff --git a/activerecord/lib/active_record/connection_adapters/mysql/column.rb b/activerecord/lib/active_record/connection_adapters/mysql/column.rb new file mode 100644 index 0000000000..1499c1681f --- /dev/null +++ b/activerecord/lib/active_record/connection_adapters/mysql/column.rb @@ -0,0 +1,21 @@ +module ActiveRecord + module ConnectionAdapters + module MySQL + class Column < ConnectionAdapters::Column # :nodoc: + delegate :extra, to: :sql_type_metadata, allow_nil: true + + def unsigned? + /\bunsigned(?: zerofill)?\z/.match?(sql_type) + end + + def case_sensitive? + collation && !/_ci\z/.match?(collation) + end + + def auto_increment? + extra == "auto_increment" + end + end + end + end +end diff --git a/activerecord/lib/active_record/connection_adapters/mysql/database_statements.rb b/activerecord/lib/active_record/connection_adapters/mysql/database_statements.rb new file mode 100644 index 0000000000..78e7181266 --- /dev/null +++ b/activerecord/lib/active_record/connection_adapters/mysql/database_statements.rb @@ -0,0 +1,108 @@ +module ActiveRecord + module ConnectionAdapters + module MySQL + module DatabaseStatements + # Returns an ActiveRecord::Result instance. + def select_all(arel, name = nil, binds = [], preparable: nil) + result = if ExplainRegistry.collect? && prepared_statements + unprepared_statement { super } + else + super + end + @connection.next_result while @connection.more_results? + result + end + + # Returns an array of arrays containing the field values. + # Order is the same as that returned by +columns+. + def select_rows(sql, name = nil, binds = []) + select_result(sql, name, binds) do |result| + @connection.next_result while @connection.more_results? + result.to_a + end + end + + # Executes the SQL statement in the context of this connection. + def execute(sql, name = nil) + # make sure we carry over any changes to ActiveRecord::Base.default_timezone that have been + # made since we established the connection + @connection.query_options[:database_timezone] = ActiveRecord::Base.default_timezone + + super + end + + def exec_query(sql, name = "SQL", binds = [], prepare: false) + if without_prepared_statement?(binds) + execute_and_free(sql, name) do |result| + ActiveRecord::Result.new(result.fields, result.to_a) if result + end + else + exec_stmt_and_free(sql, name, binds, cache_stmt: prepare) do |_, result| + ActiveRecord::Result.new(result.fields, result.to_a) if result + end + end + end + + def exec_delete(sql, name = nil, binds = []) + if without_prepared_statement?(binds) + execute_and_free(sql, name) { @connection.affected_rows } + else + exec_stmt_and_free(sql, name, binds) { |stmt| stmt.affected_rows } + end + end + alias :exec_update :exec_delete + + private + + def last_inserted_id(result) + @connection.last_id + end + + def select_result(sql, name = nil, binds = []) + if without_prepared_statement?(binds) + execute_and_free(sql, name) { |result| yield result } + else + exec_stmt_and_free(sql, name, binds, cache_stmt: true) { |_, result| yield result } + end + end + + def exec_stmt_and_free(sql, name, binds, cache_stmt: false) + # make sure we carry over any changes to ActiveRecord::Base.default_timezone that have been + # made since we established the connection + @connection.query_options[:database_timezone] = ActiveRecord::Base.default_timezone + + type_casted_binds = type_casted_binds(binds) + + log(sql, name, binds, type_casted_binds) do + if cache_stmt + cache = @statements[sql] ||= { + stmt: @connection.prepare(sql) + } + stmt = cache[:stmt] + else + stmt = @connection.prepare(sql) + end + + begin + result = ActiveSupport::Dependencies.interlock.permit_concurrent_loads do + stmt.execute(*type_casted_binds) + end + rescue Mysql2::Error => e + if cache_stmt + @statements.delete(sql) + else + stmt.close + end + raise e + end + + ret = yield stmt, result + result.free if result + stmt.close unless cache_stmt + ret + end + end + end + end + end +end diff --git a/activerecord/lib/active_record/connection_adapters/mysql/explain_pretty_printer.rb b/activerecord/lib/active_record/connection_adapters/mysql/explain_pretty_printer.rb new file mode 100644 index 0000000000..9691060cd3 --- /dev/null +++ b/activerecord/lib/active_record/connection_adapters/mysql/explain_pretty_printer.rb @@ -0,0 +1,70 @@ +module ActiveRecord + module ConnectionAdapters + module MySQL + class ExplainPrettyPrinter # :nodoc: + # Pretty prints the result of an EXPLAIN in a way that resembles the output of the + # MySQL shell: + # + # +----+-------------+-------+-------+---------------+---------+---------+-------+------+-------------+ + # | id | select_type | table | type | possible_keys | key | key_len | ref | rows | Extra | + # +----+-------------+-------+-------+---------------+---------+---------+-------+------+-------------+ + # | 1 | SIMPLE | users | const | PRIMARY | PRIMARY | 4 | const | 1 | | + # | 1 | SIMPLE | posts | ALL | NULL | NULL | NULL | NULL | 1 | Using where | + # +----+-------------+-------+-------+---------------+---------+---------+-------+------+-------------+ + # 2 rows in set (0.00 sec) + # + # This is an exercise in Ruby hyperrealism :). + def pp(result, elapsed) + widths = compute_column_widths(result) + separator = build_separator(widths) + + pp = [] + + pp << separator + pp << build_cells(result.columns, widths) + pp << separator + + result.rows.each do |row| + pp << build_cells(row, widths) + end + + pp << separator + pp << build_footer(result.rows.length, elapsed) + + pp.join("\n") + "\n" + end + + private + + def compute_column_widths(result) + [].tap do |widths| + result.columns.each_with_index do |column, i| + cells_in_column = [column] + result.rows.map { |r| r[i].nil? ? "NULL" : r[i].to_s } + widths << cells_in_column.map(&:length).max + end + end + end + + def build_separator(widths) + padding = 1 + "+" + widths.map { |w| "-" * (w + (padding * 2)) }.join("+") + "+" + end + + def build_cells(items, widths) + cells = [] + items.each_with_index do |item, i| + item = "NULL" if item.nil? + justifier = item.is_a?(Numeric) ? "rjust" : "ljust" + cells << item.to_s.send(justifier, widths[i]) + end + "| " + cells.join(" | ") + " |" + end + + def build_footer(nrows, elapsed) + rows_label = nrows == 1 ? "row" : "rows" + "#{nrows} #{rows_label} in set (%.2f sec)" % elapsed + end + end + end + end +end diff --git a/activerecord/lib/active_record/connection_adapters/mysql/quoting.rb b/activerecord/lib/active_record/connection_adapters/mysql/quoting.rb new file mode 100644 index 0000000000..9d11ad28d4 --- /dev/null +++ b/activerecord/lib/active_record/connection_adapters/mysql/quoting.rb @@ -0,0 +1,51 @@ +module ActiveRecord + module ConnectionAdapters + module MySQL + module Quoting # :nodoc: + QUOTED_TRUE, QUOTED_FALSE = "1".freeze, "0".freeze + + def quote_column_name(name) + @quoted_column_names[name] ||= "`#{super.gsub('`', '``')}`".freeze + end + + def quote_table_name(name) + @quoted_table_names[name] ||= super.gsub(".", "`.`").freeze + end + + def quoted_true + QUOTED_TRUE + end + + def unquoted_true + 1 + end + + def quoted_false + QUOTED_FALSE + end + + def unquoted_false + 0 + end + + def quoted_date(value) + if supports_datetime_with_precision? + super + else + super.sub(/\.\d{6}\z/, "") + end + end + + private + + def _quote(value) + if value.is_a?(Type::Binary::Data) + "x'#{value.hex}'" + else + super + end + end + end + end + end +end diff --git a/activerecord/lib/active_record/connection_adapters/mysql/schema_creation.rb b/activerecord/lib/active_record/connection_adapters/mysql/schema_creation.rb new file mode 100644 index 0000000000..d808b50332 --- /dev/null +++ b/activerecord/lib/active_record/connection_adapters/mysql/schema_creation.rb @@ -0,0 +1,67 @@ +module ActiveRecord + module ConnectionAdapters + module MySQL + class SchemaCreation < AbstractAdapter::SchemaCreation + delegate :add_sql_comment!, to: :@conn + private :add_sql_comment! + + private + + def visit_DropForeignKey(name) + "DROP FOREIGN KEY #{name}" + end + + def visit_ColumnDefinition(o) + o.sql_type = type_to_sql(o.type, o.limit, o.precision, o.scale, o.unsigned) + super + end + + def visit_AddColumnDefinition(o) + add_column_position!(super, column_options(o.column)) + end + + def visit_ChangeColumnDefinition(o) + change_column_sql = "CHANGE #{quote_column_name(o.name)} #{accept(o.column)}" + add_column_position!(change_column_sql, column_options(o.column)) + end + + def add_table_options!(create_sql, options) + add_sql_comment!(super, options[:comment]) + end + + def column_options(o) + column_options = super + column_options[:charset] = o.charset + column_options + end + + def add_column_options!(sql, options) + if charset = options[:charset] + sql << " CHARACTER SET #{charset}" + end + + if collation = options[:collation] + sql << " COLLATE #{collation}" + end + + add_sql_comment!(super, options[:comment]) + end + + def add_column_position!(sql, options) + if options[:first] + sql << " FIRST" + elsif options[:after] + sql << " AFTER #{quote_column_name(options[:after])}" + end + + sql + end + + def index_in_create(table_name, column_name, options) + index_name, index_type, index_columns, _, _, index_using, comment = @conn.add_index_options(table_name, column_name, options) + add_sql_comment!("#{index_type} INDEX #{quote_column_name(index_name)} #{index_using} (#{index_columns})", comment) + end + end + end + end +end diff --git a/activerecord/lib/active_record/connection_adapters/mysql/schema_definitions.rb b/activerecord/lib/active_record/connection_adapters/mysql/schema_definitions.rb new file mode 100644 index 0000000000..0cf40de70f --- /dev/null +++ b/activerecord/lib/active_record/connection_adapters/mysql/schema_definitions.rb @@ -0,0 +1,96 @@ +module ActiveRecord + module ConnectionAdapters + module MySQL + module ColumnMethods + def primary_key(name, type = :primary_key, **options) + if type == :primary_key && !options.key?(:default) + options[:auto_increment] = true + options[:limit] = 8 + end + super + end + + def blob(*args, **options) + args.each { |name| column(name, :blob, options) } + end + + def tinyblob(*args, **options) + args.each { |name| column(name, :tinyblob, options) } + end + + def mediumblob(*args, **options) + args.each { |name| column(name, :mediumblob, options) } + end + + def longblob(*args, **options) + args.each { |name| column(name, :longblob, options) } + end + + def tinytext(*args, **options) + args.each { |name| column(name, :tinytext, options) } + end + + def mediumtext(*args, **options) + args.each { |name| column(name, :mediumtext, options) } + end + + def longtext(*args, **options) + args.each { |name| column(name, :longtext, options) } + end + + def json(*args, **options) + args.each { |name| column(name, :json, options) } + end + + def unsigned_integer(*args, **options) + args.each { |name| column(name, :unsigned_integer, options) } + end + + def unsigned_bigint(*args, **options) + args.each { |name| column(name, :unsigned_bigint, options) } + end + + def unsigned_float(*args, **options) + args.each { |name| column(name, :unsigned_float, options) } + end + + def unsigned_decimal(*args, **options) + args.each { |name| column(name, :unsigned_decimal, options) } + end + end + + class ColumnDefinition < ActiveRecord::ConnectionAdapters::ColumnDefinition + attr_accessor :charset, :unsigned + end + + class TableDefinition < ActiveRecord::ConnectionAdapters::TableDefinition + include ColumnMethods + + def new_column_definition(name, type, options) # :nodoc: + column = super + case column.type + when :primary_key + column.type = :integer + column.auto_increment = true + when /\Aunsigned_(?<type>.+)\z/ + column.type = $~[:type].to_sym + column.unsigned = true + end + column.unsigned ||= options[:unsigned] + column.charset = options[:charset] + column + end + + private + + def create_column_definition(name, type) + MySQL::ColumnDefinition.new(name, type) + end + end + + class Table < ActiveRecord::ConnectionAdapters::Table + include ColumnMethods + end + end + end +end diff --git a/activerecord/lib/active_record/connection_adapters/mysql/schema_dumper.rb b/activerecord/lib/active_record/connection_adapters/mysql/schema_dumper.rb new file mode 100644 index 0000000000..2065816501 --- /dev/null +++ b/activerecord/lib/active_record/connection_adapters/mysql/schema_dumper.rb @@ -0,0 +1,52 @@ +module ActiveRecord + module ConnectionAdapters + module MySQL + module ColumnDumper + def column_spec_for_primary_key(column) + spec = super + if column.type == :integer && !column.auto_increment? + spec[:default] = schema_default(column) || "nil" + end + spec[:unsigned] = "true" if column.unsigned? + spec + end + + def prepare_column_options(column) + spec = super + spec[:unsigned] = "true" if column.unsigned? + spec + end + + def migration_keys + super + [:unsigned] + end + + private + + def default_primary_key?(column) + super && column.auto_increment? + end + + def schema_type(column) + if column.sql_type == "tinyblob" + :blob + else + super + end + end + + def schema_precision(column) + super unless /time/.match?(column.sql_type) && column.precision == 0 + end + + def schema_collation(column) + if column.collation && table_name = column.table_name + @table_collation_cache ||= {} + @table_collation_cache[table_name] ||= select_one("SHOW TABLE STATUS LIKE '#{table_name}'")["Collation"] + column.collation.inspect if column.collation != @table_collation_cache[table_name] + end + end + end + end + end +end diff --git a/activerecord/lib/active_record/connection_adapters/mysql/type_metadata.rb b/activerecord/lib/active_record/connection_adapters/mysql/type_metadata.rb new file mode 100644 index 0000000000..24dcf852e1 --- /dev/null +++ b/activerecord/lib/active_record/connection_adapters/mysql/type_metadata.rb @@ -0,0 +1,31 @@ +module ActiveRecord + module ConnectionAdapters + module MySQL + class TypeMetadata < DelegateClass(SqlTypeMetadata) # :nodoc: + attr_reader :extra + + def initialize(type_metadata, extra: "") + super(type_metadata) + @type_metadata = type_metadata + @extra = extra + end + + def ==(other) + other.is_a?(MySQL::TypeMetadata) && + attributes_for_hash == other.attributes_for_hash + end + alias eql? == + + def hash + attributes_for_hash.hash + end + + protected + + def attributes_for_hash + [self.class, @type_metadata, extra] + end + end + end + end +end diff --git a/activerecord/lib/active_record/connection_adapters/mysql2_adapter.rb b/activerecord/lib/active_record/connection_adapters/mysql2_adapter.rb index e97e82f056..45e400b75b 100644 --- a/activerecord/lib/active_record/connection_adapters/mysql2_adapter.rb +++ b/activerecord/lib/active_record/connection_adapters/mysql2_adapter.rb @@ -1,7 +1,9 @@ -require 'active_record/connection_adapters/abstract_mysql_adapter' +require "active_record/connection_adapters/abstract_mysql_adapter" +require "active_record/connection_adapters/mysql/database_statements" -gem 'mysql2', '~> 0.3.18' -require 'mysql2' +gem "mysql2", ">= 0.3.18", "< 0.5" +require "mysql2" +raise "mysql2 0.4.3 is not supported. Please upgrade to 0.4.4+" if Mysql2::VERSION == "0.4.3" module ActiveRecord module ConnectionHandling # :nodoc: @@ -9,18 +11,20 @@ module ActiveRecord def mysql2_connection(config) config = config.symbolize_keys - config[:username] = 'root' if config[:username].nil? + config[:username] = "root" if config[:username].nil? + config[:flags] ||= 0 - if Mysql2::Client.const_defined? :FOUND_ROWS - config[:flags] = Mysql2::Client::FOUND_ROWS + if config[:flags].kind_of? Array + config[:flags].push "FOUND_ROWS".freeze + else + config[:flags] |= Mysql2::Client::FOUND_ROWS end client = Mysql2::Client.new(config) - options = [config[:host], config[:username], config[:password], config[:database], config[:port], config[:socket], 0] - ConnectionAdapters::Mysql2Adapter.new(client, logger, options, config) + ConnectionAdapters::Mysql2Adapter.new(client, logger, nil, config) rescue Mysql2::Error => error if error.message.include?("Unknown database") - raise ActiveRecord::NoDatabaseError.new(error.message, error) + raise ActiveRecord::NoDatabaseError else raise end @@ -29,15 +33,29 @@ module ActiveRecord module ConnectionAdapters class Mysql2Adapter < AbstractMysqlAdapter - ADAPTER_NAME = 'Mysql2'.freeze + ADAPTER_NAME = "Mysql2".freeze + + include MySQL::DatabaseStatements def initialize(connection, logger, connection_options, config) super - @prepared_statements = false + @prepared_statements = false unless config.key?(:prepared_statements) configure_connection end - def supports_explain? + def supports_json? + !mariadb? && version >= "5.7.8" + end + + def supports_comments? + true + end + + def supports_comments_in_create? + true + end + + def supports_savepoints? true end @@ -45,7 +63,7 @@ module ActiveRecord def each_hash(result) # :nodoc: if block_given? - result.each(:as => :hash, :symbolize_keys => true) do |row| + result.each(as: :hash, symbolize_keys: true) do |row| yield row end else @@ -70,7 +88,6 @@ module ActiveRecord #++ def active? - return false unless @connection @connection.ping end @@ -85,181 +102,24 @@ module ActiveRecord # Otherwise, this method does nothing. def disconnect! super - unless @connection.nil? - @connection.close - @connection = nil - end - end - - #-- - # DATABASE STATEMENTS ====================================== - #++ - - def explain(arel, binds = []) - sql = "EXPLAIN #{to_sql(arel, binds.dup)}" - start = Time.now - result = exec_query(sql, 'EXPLAIN', binds) - elapsed = Time.now - start - - ExplainPrettyPrinter.new.pp(result, elapsed) + @connection.close end - class ExplainPrettyPrinter # :nodoc: - # Pretty prints the result of a EXPLAIN in a way that resembles the output of the - # MySQL shell: - # - # +----+-------------+-------+-------+---------------+---------+---------+-------+------+-------------+ - # | id | select_type | table | type | possible_keys | key | key_len | ref | rows | Extra | - # +----+-------------+-------+-------+---------------+---------+---------+-------+------+-------------+ - # | 1 | SIMPLE | users | const | PRIMARY | PRIMARY | 4 | const | 1 | | - # | 1 | SIMPLE | posts | ALL | NULL | NULL | NULL | NULL | 1 | Using where | - # +----+-------------+-------+-------+---------------+---------+---------+-------+------+-------------+ - # 2 rows in set (0.00 sec) - # - # This is an exercise in Ruby hyperrealism :). - def pp(result, elapsed) - widths = compute_column_widths(result) - separator = build_separator(widths) - - pp = [] - - pp << separator - pp << build_cells(result.columns, widths) - pp << separator - - result.rows.each do |row| - pp << build_cells(row, widths) - end - - pp << separator - pp << build_footer(result.rows.length, elapsed) - - pp.join("\n") + "\n" - end - - private - - def compute_column_widths(result) - [].tap do |widths| - result.columns.each_with_index do |column, i| - cells_in_column = [column] + result.rows.map {|r| r[i].nil? ? 'NULL' : r[i].to_s} - widths << cells_in_column.map(&:length).max - end - end - end + private - def build_separator(widths) - padding = 1 - '+' + widths.map {|w| '-' * (w + (padding*2))}.join('+') + '+' + def connect + @connection = Mysql2::Client.new(@config) + configure_connection end - def build_cells(items, widths) - cells = [] - items.each_with_index do |item, i| - item = 'NULL' if item.nil? - justifier = item.is_a?(Numeric) ? 'rjust' : 'ljust' - cells << item.to_s.send(justifier, widths[i]) - end - '| ' + cells.join(' | ') + ' |' + def configure_connection + @connection.query_options.merge!(as: :array) + super end - def build_footer(nrows, elapsed) - rows_label = nrows == 1 ? 'row' : 'rows' - "#{nrows} #{rows_label} in set (%.2f sec)" % elapsed + def full_version + @full_version ||= @connection.server_info[:version] end - end - - # FIXME: re-enable the following once a "better" query_cache solution is in core - # - # The overrides below perform much better than the originals in AbstractAdapter - # because we're able to take advantage of mysql2's lazy-loading capabilities - # - # # Returns a record hash with the column names as keys and column values - # # as values. - # def select_one(sql, name = nil) - # result = execute(sql, name) - # result.each(as: :hash) do |r| - # return r - # end - # end - # - # # Returns a single value from a record - # def select_value(sql, name = nil) - # result = execute(sql, name) - # if first = result.first - # first.first - # end - # end - # - # # Returns an array of the values of the first column in a select: - # # select_values("SELECT id FROM companies LIMIT 3") => [1,2,3] - # def select_values(sql, name = nil) - # execute(sql, name).map { |row| row.first } - # end - - # Returns an array of arrays containing the field values. - # Order is the same as that returned by +columns+. - def select_rows(sql, name = nil, binds = []) - execute(sql, name).to_a - end - - # Executes the SQL statement in the context of this connection. - def execute(sql, name = nil) - if @connection - # make sure we carry over any changes to ActiveRecord::Base.default_timezone that have been - # made since we established the connection - @connection.query_options[:database_timezone] = ActiveRecord::Base.default_timezone - end - - super - end - - def exec_query(sql, name = 'SQL', binds = []) - result = execute(sql, name) - ActiveRecord::Result.new(result.fields, result.to_a) - end - - alias exec_without_stmt exec_query - - def insert_sql(sql, name = nil, pk = nil, id_value = nil, sequence_name = nil) - super - id_value || @connection.last_id - end - alias :create :insert_sql - - def exec_insert(sql, name, binds, pk = nil, sequence_name = nil) - execute to_sql(sql, binds), name - end - - def exec_delete(sql, name, binds) - execute to_sql(sql, binds), name - @connection.affected_rows - end - alias :exec_update :exec_delete - - def last_inserted_id(result) - @connection.last_id - end - - private - - def connect - @connection = Mysql2::Client.new(@config) - configure_connection - end - - def configure_connection - @connection.query_options.merge!(:as => :array) - super - end - - def full_version - @full_version ||= @connection.info[:version] - end - - def set_field_encoding field_name - field_name - end end end end diff --git a/activerecord/lib/active_record/connection_adapters/mysql_adapter.rb b/activerecord/lib/active_record/connection_adapters/mysql_adapter.rb deleted file mode 100644 index 2ae462d773..0000000000 --- a/activerecord/lib/active_record/connection_adapters/mysql_adapter.rb +++ /dev/null @@ -1,468 +0,0 @@ -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.9' -require 'mysql' - -class Mysql # :nodoc: all - class Time - # Used for casting DateTime fields to a MySQL friendly Time. - # This was documented in 48498da0dfed5239ea1eafb243ce47d7e3ce9e8e - def to_date - Date.new(year, month, day) - end - end - class Stmt; include Enumerable end - class Result; include Enumerable end -end - -module ActiveRecord - module ConnectionHandling # :nodoc: - # Establishes a connection to the database that's used by all Active Record objects. - def mysql_connection(config) - config = config.symbolize_keys - host = config[:host] - port = config[:port] - socket = config[:socket] - username = config[:username] ? config[:username].to_s : 'root' - password = config[:password].to_s - database = config[:database] - - mysql = Mysql.init - mysql.ssl_set(config[:sslkey], config[:sslcert], config[:sslca], config[:sslcapath], config[:sslcipher]) if config[:sslca] || config[:sslkey] - - default_flags = Mysql.const_defined?(:CLIENT_MULTI_RESULTS) ? Mysql::CLIENT_MULTI_RESULTS : 0 - default_flags |= Mysql::CLIENT_FOUND_ROWS if Mysql.const_defined?(:CLIENT_FOUND_ROWS) - options = [host, username, password, database, port, socket, default_flags] - ConnectionAdapters::MysqlAdapter.new(mysql, logger, options, config) - rescue Mysql::Error => error - if error.message.include?("Unknown database") - raise ActiveRecord::NoDatabaseError.new(error.message, error) - else - raise - end - end - end - - module ConnectionAdapters - # The MySQL adapter will work with both Ruby/MySQL, which is a Ruby-based MySQL adapter that comes bundled with Active Record, and with - # the faster C-based MySQL/Ruby adapter (available both as a gem and from http://www.tmtm.org/en/mysql/ruby/). - # - # Options: - # - # * <tt>:host</tt> - Defaults to "localhost". - # * <tt>:port</tt> - Defaults to 3306. - # * <tt>:socket</tt> - Defaults to "/tmp/mysql.sock". - # * <tt>:username</tt> - Defaults to "root" - # * <tt>:password</tt> - Defaults to nothing. - # * <tt>:database</tt> - The name of the database. No default, must be provided. - # * <tt>:encoding</tt> - (Optional) Sets the client encoding by executing "SET NAMES <encoding>" after connection. - # * <tt>:reconnect</tt> - Defaults to false (See MySQL documentation: http://dev.mysql.com/doc/refman/5.6/en/auto-reconnect.html). - # * <tt>:strict</tt> - Defaults to true. Enable STRICT_ALL_TABLES. (See MySQL documentation: http://dev.mysql.com/doc/refman/5.6/en/sql-mode.html) - # * <tt>:variables</tt> - (Optional) A hash session variables to send as <tt>SET @@SESSION.key = value</tt> on each database connection. Use the value +:default+ to set a variable to its DEFAULT value. (See MySQL documentation: http://dev.mysql.com/doc/refman/5.6/en/set-statement.html). - # * <tt>:sslca</tt> - Necessary to use MySQL with an SSL connection. - # * <tt>:sslkey</tt> - Necessary to use MySQL with an SSL connection. - # * <tt>:sslcert</tt> - Necessary to use MySQL with an SSL connection. - # * <tt>:sslcapath</tt> - Necessary to use MySQL with an SSL connection. - # * <tt>:sslcipher</tt> - Necessary to use MySQL with an SSL connection. - # - class MysqlAdapter < AbstractMysqlAdapter - ADAPTER_NAME = 'MySQL'.freeze - - class StatementPool < ConnectionAdapters::StatementPool - private - - def dealloc(stmt) - stmt[:stmt].close - end - end - - def initialize(connection, logger, connection_options, config) - super - @statements = StatementPool.new(@connection, - self.class.type_cast_config_to_integer(config.fetch(:statement_limit) { 1000 })) - @client_encoding = nil - connect - end - - # Returns true, since this connection adapter supports prepared statement - # caching. - def supports_statement_cache? - true - end - - # HELPER METHODS =========================================== - - def each_hash(result) # :nodoc: - if block_given? - result.each_hash do |row| - row.symbolize_keys! - yield row - end - else - to_enum(:each_hash, result) - end - end - - def error_number(exception) # :nodoc: - exception.errno if exception.respond_to?(:errno) - end - - # QUOTING ================================================== - - def quote_string(string) #:nodoc: - @connection.quote(string) - end - - #-- - # CONNECTION MANAGEMENT ==================================== - #++ - - def active? - if @connection.respond_to?(:stat) - @connection.stat - else - @connection.query 'select 1' - end - - # mysql-ruby doesn't raise an exception when stat fails. - if @connection.respond_to?(:errno) - @connection.errno.zero? - else - true - end - rescue Mysql::Error - false - end - - def reconnect! - super - disconnect! - connect - end - - # Disconnects from the database if already connected. Otherwise, this - # method does nothing. - def disconnect! - super - @connection.close rescue nil - end - - def reset! - if @connection.respond_to?(:change_user) - # See http://bugs.mysql.com/bug.php?id=33540 -- the workaround way to - # reset the connection is to change the user to the same user. - @connection.change_user(@config[:username], @config[:password], @config[:database]) - configure_connection - end - end - - #-- - # DATABASE STATEMENTS ====================================== - #++ - - def select_rows(sql, name = nil, binds = []) - @connection.query_with_result = true - rows = exec_query(sql, name, binds).rows - @connection.more_results && @connection.next_result # invoking stored procedures with CLIENT_MULTI_RESULTS requires this to tidy up else connection will be dropped - rows - end - - # Clears the prepared statements cache. - def clear_cache! - super - @statements.clear - end - - # Taken from here: - # https://github.com/tmtm/ruby-mysql/blob/master/lib/mysql/charset.rb - # Author: TOMITA Masahiro <tommy@tmtm.org> - ENCODINGS = { - "armscii8" => nil, - "ascii" => Encoding::US_ASCII, - "big5" => Encoding::Big5, - "binary" => Encoding::ASCII_8BIT, - "cp1250" => Encoding::Windows_1250, - "cp1251" => Encoding::Windows_1251, - "cp1256" => Encoding::Windows_1256, - "cp1257" => Encoding::Windows_1257, - "cp850" => Encoding::CP850, - "cp852" => Encoding::CP852, - "cp866" => Encoding::IBM866, - "cp932" => Encoding::Windows_31J, - "dec8" => nil, - "eucjpms" => Encoding::EucJP_ms, - "euckr" => Encoding::EUC_KR, - "gb2312" => Encoding::EUC_CN, - "gbk" => Encoding::GBK, - "geostd8" => nil, - "greek" => Encoding::ISO_8859_7, - "hebrew" => Encoding::ISO_8859_8, - "hp8" => nil, - "keybcs2" => nil, - "koi8r" => Encoding::KOI8_R, - "koi8u" => Encoding::KOI8_U, - "latin1" => Encoding::ISO_8859_1, - "latin2" => Encoding::ISO_8859_2, - "latin5" => Encoding::ISO_8859_9, - "latin7" => Encoding::ISO_8859_13, - "macce" => Encoding::MacCentEuro, - "macroman" => Encoding::MacRoman, - "sjis" => Encoding::SHIFT_JIS, - "swe7" => nil, - "tis620" => Encoding::TIS_620, - "ucs2" => Encoding::UTF_16BE, - "ujis" => Encoding::EucJP_ms, - "utf8" => Encoding::UTF_8, - "utf8mb4" => Encoding::UTF_8, - } - - # Get the client encoding for this database - def client_encoding - return @client_encoding if @client_encoding - - result = exec_query( - "SHOW VARIABLES WHERE Variable_name = 'character_set_client'", - 'SCHEMA') - @client_encoding = ENCODINGS[result.rows.last.last] - end - - def exec_query(sql, name = 'SQL', binds = []) - if without_prepared_statement?(binds) - result_set, affected_rows = exec_without_stmt(sql, name) - else - result_set, affected_rows = exec_stmt(sql, name, binds) - end - - yield affected_rows if block_given? - - result_set - end - - def last_inserted_id(result) - @connection.insert_id - end - - module Fields # :nodoc: - class DateTime < Type::DateTime # :nodoc: - def cast_value(value) - if Mysql::Time === value - new_time( - value.year, - value.month, - value.day, - value.hour, - value.minute, - value.second, - value.second_part) - else - super - end - end - end - - class Time < Type::Time # :nodoc: - def cast_value(value) - if Mysql::Time === value - new_time( - 2000, - 01, - 01, - value.hour, - value.minute, - value.second, - value.second_part) - else - super - end - end - end - - class << self - TYPES = Type::HashLookupTypeMap.new # :nodoc: - - delegate :register_type, :alias_type, to: :TYPES - - def find_type(field) - if field.type == Mysql::Field::TYPE_TINY && field.length > 1 - TYPES.lookup(Mysql::Field::TYPE_LONG) - else - TYPES.lookup(field.type) - end - end - end - - register_type Mysql::Field::TYPE_TINY, Type::Boolean.new - register_type Mysql::Field::TYPE_LONG, Type::Integer.new - alias_type Mysql::Field::TYPE_LONGLONG, Mysql::Field::TYPE_LONG - alias_type Mysql::Field::TYPE_NEWDECIMAL, Mysql::Field::TYPE_LONG - - register_type Mysql::Field::TYPE_DATE, Type::Date.new - register_type Mysql::Field::TYPE_DATETIME, Fields::DateTime.new - register_type Mysql::Field::TYPE_TIME, Fields::Time.new - register_type Mysql::Field::TYPE_FLOAT, Type::Float.new - end - - def initialize_type_map(m) # :nodoc: - super - register_class_with_precision m, %r(datetime)i, Fields::DateTime - register_class_with_precision m, %r(time)i, Fields::Time - end - - def exec_without_stmt(sql, name = 'SQL') # :nodoc: - # Some queries, like SHOW CREATE TABLE don't work through the prepared - # statement API. For those queries, we need to use this method. :'( - log(sql, name) do - result = @connection.query(sql) - affected_rows = @connection.affected_rows - - if result - types = {} - fields = [] - result.fetch_fields.each { |field| - field_name = field.name - fields << field_name - - if field.decimals > 0 - types[field_name] = Type::Decimal.new - else - types[field_name] = Fields.find_type field - end - } - - result_set = ActiveRecord::Result.new(fields, result.to_a, types) - result.free - else - result_set = ActiveRecord::Result.new([], []) - end - - [result_set, affected_rows] - end - end - - def execute_and_free(sql, name = nil) # :nodoc: - result = execute(sql, name) - ret = yield result - result.free - ret - end - - def insert_sql(sql, name = nil, pk = nil, id_value = nil, sequence_name = nil) #:nodoc: - super sql, name - id_value || @connection.insert_id - end - alias :create :insert_sql - - def exec_delete(sql, name, binds) # :nodoc: - affected_rows = 0 - - exec_query(sql, name, binds) do |n| - affected_rows = n - end - - affected_rows - end - alias :exec_update :exec_delete - - def begin_db_transaction #:nodoc: - exec_query "BEGIN" - end - - private - - def exec_stmt(sql, name, binds) - cache = {} - type_casted_binds = binds.map { |attr| type_cast(attr.value_for_database) } - - log(sql, name, binds) do - if binds.empty? - stmt = @connection.prepare(sql) - else - cache = @statements[sql] ||= { - :stmt => @connection.prepare(sql) - } - stmt = cache[:stmt] - end - - begin - stmt.execute(*type_casted_binds) - rescue Mysql::Error => e - # Older versions of MySQL leave the prepared statement in a bad - # place when an error occurs. To support older MySQL versions, we - # need to close the statement and delete the statement from the - # cache. - if binds.empty? - stmt.close - else - @statements.delete sql - end - raise e - end - - cols = nil - if metadata = stmt.result_metadata - cols = cache[:cols] ||= metadata.fetch_fields.map(&:name) - metadata.free - end - - result_set = ActiveRecord::Result.new(cols, stmt.to_a) if cols - affected_rows = stmt.affected_rows - - stmt.free_result - stmt.close if binds.empty? - - [result_set, affected_rows] - end - end - - def connect - encoding = @config[:encoding] - if encoding - @connection.options(Mysql::SET_CHARSET_NAME, encoding) rescue nil - end - - if @config[:sslca] || @config[:sslkey] - @connection.ssl_set(@config[:sslkey], @config[:sslcert], @config[:sslca], @config[:sslcapath], @config[:sslcipher]) - end - - @connection.options(Mysql::OPT_CONNECT_TIMEOUT, @config[:connect_timeout]) if @config[:connect_timeout] - @connection.options(Mysql::OPT_READ_TIMEOUT, @config[:read_timeout]) if @config[:read_timeout] - @connection.options(Mysql::OPT_WRITE_TIMEOUT, @config[:write_timeout]) if @config[:write_timeout] - - @connection.real_connect(*@connection_options) - - # reconnect must be set after real_connect is called, because real_connect sets it to false internally - @connection.reconnect = !!@config[:reconnect] if @connection.respond_to?(:reconnect=) - - configure_connection - end - - # Many Rails applications monkey-patch a replacement of the configure_connection method - # and don't call 'super', so leave this here even though it looks superfluous. - def configure_connection - super - end - - def select(sql, name = nil, binds = []) - @connection.query_with_result = true - rows = super - @connection.more_results && @connection.next_result # invoking stored procedures with CLIENT_MULTI_RESULTS requires this to tidy up else connection will be dropped - rows - end - - # Returns the full version of the connected MySQL server. - def full_version - @full_version ||= @connection.server_info - end - - def set_field_encoding field_name - field_name.force_encoding(client_encoding) - if internal_enc = Encoding.default_internal - field_name = field_name.encode!(internal_enc) - end - field_name - end - end - end -end diff --git a/activerecord/lib/active_record/connection_adapters/postgresql/column.rb b/activerecord/lib/active_record/connection_adapters/postgresql/column.rb index bfa03fa136..3ad1911a28 100644 --- a/activerecord/lib/active_record/connection_adapters/postgresql/column.rb +++ b/activerecord/lib/active_record/connection_adapters/postgresql/column.rb @@ -8,7 +8,6 @@ module ActiveRecord def serial? return unless default_function - table_name = @table_name || '(?<table_name>.+)' %r{\Anextval\('"?#{table_name}_#{name}_seq"?'::regclass\)\z} === default_function end end diff --git a/activerecord/lib/active_record/connection_adapters/postgresql/database_statements.rb b/activerecord/lib/active_record/connection_adapters/postgresql/database_statements.rb index 11d3f5301a..9a2017b443 100644 --- a/activerecord/lib/active_record/connection_adapters/postgresql/database_statements.rb +++ b/activerecord/lib/active_record/connection_adapters/postgresql/database_statements.rb @@ -4,44 +4,7 @@ module ActiveRecord module DatabaseStatements def explain(arel, binds = []) sql = "EXPLAIN #{to_sql(arel, binds)}" - ExplainPrettyPrinter.new.pp(exec_query(sql, 'EXPLAIN', binds)) - end - - class ExplainPrettyPrinter # :nodoc: - # Pretty prints the result of a EXPLAIN in a way that resembles the output of the - # PostgreSQL shell: - # - # QUERY PLAN - # ------------------------------------------------------------------------------ - # Nested Loop Left Join (cost=0.00..37.24 rows=8 width=0) - # Join Filter: (posts.user_id = users.id) - # -> Index Scan using users_pkey on users (cost=0.00..8.27 rows=1 width=4) - # Index Cond: (id = 1) - # -> Seq Scan on posts (cost=0.00..28.88 rows=8 width=4) - # Filter: (posts.user_id = 1) - # (6 rows) - # - def pp(result) - header = result.columns.first - lines = result.rows.map(&:first) - - # We add 2 because there's one char of padding at both sides, note - # the extra hyphens in the example above. - width = [header, *lines].map(&:length).max + 2 - - pp = [] - - pp << header.center(width).rstrip - pp << '-' * width - - pp += lines.map {|line| " #{line}"} - - nrows = result.rows.length - rows_label = nrows == 1 ? 'row' : 'rows' - pp << "(#{nrows} #{rows_label})" - - pp.join("\n") + "\n" - end + PostgreSQL::ExplainPrettyPrinter.new.pp(exec_query(sql, "EXPLAIN", binds)) end def select_value(arel, name = nil, binds = []) @@ -52,8 +15,8 @@ module ActiveRecord end end - def select_values(arel, name = nil) - arel, binds = binds_from_relation arel, [] + def select_values(arel, name = nil, binds = []) + arel, binds = binds_from_relation arel, binds sql = to_sql(arel, binds) execute_and_clear(sql, name, binds) do |result| if result.nfields > 0 @@ -72,28 +35,6 @@ module ActiveRecord end end - # Executes an INSERT query and returns the new record's ID - def insert_sql(sql, name = nil, pk = nil, id_value = nil, sequence_name = nil) - unless pk - # Extract the table from the insert sql. Yuck. - table_ref = extract_table_ref_from_insert_sql(sql) - pk = primary_key(table_ref) if table_ref - end - - if pk && use_insert_returning? - select_value("#{sql} RETURNING #{quote_column_name(pk)}") - elsif pk - super - last_insert_id_value(sequence_name || default_sequence_name(table_ref, pk)) - else - super - end - end - - def create - super.insert - end - # The internal PostgreSQL identifier of the money data type. MONEY_COLUMN_TYPE_OID = 790 #:nodoc: # The internal PostgreSQL identifier of the BYTEA data type. @@ -133,9 +74,9 @@ module ActiveRecord # (2) $12.345.678,12 case data when /^-?\D+[\d,]+\.\d{2}$/ # (1) - data.gsub!(/[^-\d.]/, '') + data.gsub!(/[^-\d.]/, "") when /^-?\D+[\d.]+,\d{2}$/ # (2) - data.gsub!(/[^-\d,]/, '').sub!(/,/, '.') + data.gsub!(/[^-\d,]/, "").sub!(/,/, ".") end end end @@ -144,20 +85,26 @@ module ActiveRecord # Queries the database and returns the results in an Array-like object def query(sql, name = nil) #:nodoc: log(sql, name) do - result_as_array @connection.async_exec(sql) + ActiveSupport::Dependencies.interlock.permit_concurrent_loads do + result_as_array @connection.async_exec(sql) + end end end - # Executes an SQL statement, returning a PGresult object on success - # or raising a PGError exception otherwise. + # Executes an SQL statement, returning a PG::Result object on success + # or raising a PG::Error exception otherwise. + # Note: the PG::Result object is manually memory managed; if you don't + # need it specifically, you may want consider the <tt>exec_query</tt> wrapper. def execute(sql, name = nil) log(sql, name) do - @connection.async_exec(sql) + ActiveSupport::Dependencies.interlock.permit_concurrent_loads do + @connection.async_exec(sql) + end end end - def exec_query(sql, name = 'SQL', binds = []) - execute_and_clear(sql, name, binds) do |result| + def exec_query(sql, name = "SQL", binds = [], prepare: false) + execute_and_clear(sql, name, binds, prepare: prepare) do |result| types = {} fields = result.fields fields.each_with_index do |fname, i| @@ -169,44 +116,44 @@ module ActiveRecord end end - def exec_delete(sql, name = 'SQL', binds = []) - execute_and_clear(sql, name, binds) {|result| result.cmd_tuples } + def exec_delete(sql, name = nil, binds = []) + execute_and_clear(sql, name, binds) { |result| result.cmd_tuples } end alias :exec_update :exec_delete - def sql_for_insert(sql, pk, id_value, sequence_name, binds) - unless pk + def sql_for_insert(sql, pk, id_value, sequence_name, binds) # :nodoc: + if pk.nil? # Extract the table from the insert sql. Yuck. table_ref = extract_table_ref_from_insert_sql(sql) pk = primary_key(table_ref) if table_ref end - if pk && use_insert_returning? + if pk = suppress_composite_primary_key(pk) sql = "#{sql} RETURNING #{quote_column_name(pk)}" end - [sql, binds] + super end + private :sql_for_insert - def exec_insert(sql, name, binds, pk = nil, sequence_name = nil) - val = exec_query(sql, name, binds) - if !use_insert_returning? && pk + def exec_insert(sql, name = nil, binds = [], pk = nil, sequence_name = nil) + if use_insert_returning? || pk == false + super + else + result = exec_query(sql, name, binds) unless sequence_name table_ref = extract_table_ref_from_insert_sql(sql) - sequence_name = default_sequence_name(table_ref, pk) - return val unless sequence_name + if table_ref + pk = primary_key(table_ref) if pk.nil? + pk = suppress_composite_primary_key(pk) + sequence_name = default_sequence_name(table_ref, pk) + end + return result unless sequence_name end last_insert_id_result(sequence_name) - else - val end end - # Executes an UPDATE query and returns the number of affected tuples. - def update_sql(sql, name = nil) - super.cmd_tuples - end - # Begins a transaction. def begin_db_transaction execute "BEGIN" @@ -226,6 +173,12 @@ module ActiveRecord def exec_rollback_db_transaction execute "ROLLBACK" end + + private + + def suppress_composite_primary_key(pk) + pk unless pk.is_a?(Array) + end end end end diff --git a/activerecord/lib/active_record/connection_adapters/postgresql/explain_pretty_printer.rb b/activerecord/lib/active_record/connection_adapters/postgresql/explain_pretty_printer.rb new file mode 100644 index 0000000000..99f3a5bbdf --- /dev/null +++ b/activerecord/lib/active_record/connection_adapters/postgresql/explain_pretty_printer.rb @@ -0,0 +1,42 @@ +module ActiveRecord + module ConnectionAdapters + module PostgreSQL + class ExplainPrettyPrinter # :nodoc: + # Pretty prints the result of an EXPLAIN in a way that resembles the output of the + # PostgreSQL shell: + # + # QUERY PLAN + # ------------------------------------------------------------------------------ + # Nested Loop Left Join (cost=0.00..37.24 rows=8 width=0) + # Join Filter: (posts.user_id = users.id) + # -> Index Scan using users_pkey on users (cost=0.00..8.27 rows=1 width=4) + # Index Cond: (id = 1) + # -> Seq Scan on posts (cost=0.00..28.88 rows=8 width=4) + # Filter: (posts.user_id = 1) + # (6 rows) + # + def pp(result) + header = result.columns.first + lines = result.rows.map(&:first) + + # We add 2 because there's one char of padding at both sides, note + # the extra hyphens in the example above. + width = [header, *lines].map(&:length).max + 2 + + pp = [] + + pp << header.center(width).rstrip + pp << "-" * width + + pp += lines.map { |line| " #{line}" } + + nrows = result.rows.length + rows_label = nrows == 1 ? "row" : "rows" + pp << "(#{nrows} #{rows_label})" + + pp.join("\n") + "\n" + end + end + end + end +end diff --git a/activerecord/lib/active_record/connection_adapters/postgresql/oid.rb b/activerecord/lib/active_record/connection_adapters/postgresql/oid.rb index 68752cdd80..0e526f6201 100644 --- a/activerecord/lib/active_record/connection_adapters/postgresql/oid.rb +++ b/activerecord/lib/active_record/connection_adapters/postgresql/oid.rb @@ -1,25 +1,25 @@ -require 'active_record/connection_adapters/postgresql/oid/array' -require 'active_record/connection_adapters/postgresql/oid/bit' -require 'active_record/connection_adapters/postgresql/oid/bit_varying' -require 'active_record/connection_adapters/postgresql/oid/bytea' -require 'active_record/connection_adapters/postgresql/oid/cidr' -require 'active_record/connection_adapters/postgresql/oid/date_time' -require 'active_record/connection_adapters/postgresql/oid/decimal' -require 'active_record/connection_adapters/postgresql/oid/enum' -require 'active_record/connection_adapters/postgresql/oid/hstore' -require 'active_record/connection_adapters/postgresql/oid/inet' -require 'active_record/connection_adapters/postgresql/oid/json' -require 'active_record/connection_adapters/postgresql/oid/jsonb' -require 'active_record/connection_adapters/postgresql/oid/money' -require 'active_record/connection_adapters/postgresql/oid/point' -require 'active_record/connection_adapters/postgresql/oid/rails_5_1_point' -require 'active_record/connection_adapters/postgresql/oid/range' -require 'active_record/connection_adapters/postgresql/oid/specialized_string' -require 'active_record/connection_adapters/postgresql/oid/uuid' -require 'active_record/connection_adapters/postgresql/oid/vector' -require 'active_record/connection_adapters/postgresql/oid/xml' +require "active_record/connection_adapters/postgresql/oid/array" +require "active_record/connection_adapters/postgresql/oid/bit" +require "active_record/connection_adapters/postgresql/oid/bit_varying" +require "active_record/connection_adapters/postgresql/oid/bytea" +require "active_record/connection_adapters/postgresql/oid/cidr" +require "active_record/connection_adapters/postgresql/oid/date_time" +require "active_record/connection_adapters/postgresql/oid/decimal" +require "active_record/connection_adapters/postgresql/oid/enum" +require "active_record/connection_adapters/postgresql/oid/hstore" +require "active_record/connection_adapters/postgresql/oid/inet" +require "active_record/connection_adapters/postgresql/oid/json" +require "active_record/connection_adapters/postgresql/oid/jsonb" +require "active_record/connection_adapters/postgresql/oid/money" +require "active_record/connection_adapters/postgresql/oid/point" +require "active_record/connection_adapters/postgresql/oid/legacy_point" +require "active_record/connection_adapters/postgresql/oid/range" +require "active_record/connection_adapters/postgresql/oid/specialized_string" +require "active_record/connection_adapters/postgresql/oid/uuid" +require "active_record/connection_adapters/postgresql/oid/vector" +require "active_record/connection_adapters/postgresql/oid/xml" -require 'active_record/connection_adapters/postgresql/oid/type_map_initializer' +require "active_record/connection_adapters/postgresql/oid/type_map_initializer" module ActiveRecord module ConnectionAdapters diff --git a/activerecord/lib/active_record/connection_adapters/postgresql/oid/array.rb b/activerecord/lib/active_record/connection_adapters/postgresql/oid/array.rb index 25961a9869..d9daaaa23e 100644 --- a/activerecord/lib/active_record/connection_adapters/postgresql/oid/array.rb +++ b/activerecord/lib/active_record/connection_adapters/postgresql/oid/array.rb @@ -8,7 +8,7 @@ module ActiveRecord attr_reader :subtype, :delimiter delegate :type, :user_input_in_time_zone, :limit, to: :subtype - def initialize(subtype, delimiter = ',') + def initialize(subtype, delimiter = ",") @subtype = subtype @delimiter = delimiter @@ -33,7 +33,11 @@ module ActiveRecord def serialize(value) if value.is_a?(::Array) - @pg_encoder.encode(type_cast_array(value, :serialize)) + result = @pg_encoder.encode(type_cast_array(value, :serialize)) + if encoding = determine_encoding_of_strings(value) + result.force_encoding(encoding) + end + result else super end @@ -50,15 +54,26 @@ module ActiveRecord "[" + value.map { |v| subtype.type_cast_for_schema(v) }.join(", ") + "]" end + def map(value, &block) + value.map(&block) + end + private - def type_cast_array(value, method) - if value.is_a?(::Array) - value.map { |item| type_cast_array(item, method) } - else - @subtype.public_send(method, value) + def type_cast_array(value, method) + if value.is_a?(::Array) + value.map { |item| type_cast_array(item, method) } + else + @subtype.public_send(method, value) + end + end + + def determine_encoding_of_strings(value) + case value + when ::Array then determine_encoding_of_strings(value.first) + when ::String then value.encoding + end end - end end end end diff --git a/activerecord/lib/active_record/connection_adapters/postgresql/oid/bit.rb b/activerecord/lib/active_record/connection_adapters/postgresql/oid/bit.rb index ea0fa2517f..0a505f46a7 100644 --- a/activerecord/lib/active_record/connection_adapters/postgresql/oid/bit.rb +++ b/activerecord/lib/active_record/connection_adapters/postgresql/oid/bit.rb @@ -7,7 +7,7 @@ module ActiveRecord :bit end - def cast(value) + def cast_value(value) if ::String === value case value when /^0x/i @@ -16,7 +16,7 @@ module ActiveRecord value # Bit-string notation end else - value + value.to_s end end @@ -34,16 +34,18 @@ module ActiveRecord end def binary? - /\A[01]*\Z/ === value + /\A[01]*\Z/.match?(value) end def hex? - /\A[0-9A-F]*\Z/i === value + /\A[0-9A-F]*\Z/i.match?(value) end + # TODO Change this to private once we've dropped Ruby 2.2 support. + # Workaround for Ruby 2.2 "private attribute?" warning. protected - attr_reader :value + attr_reader :value end end end diff --git a/activerecord/lib/active_record/connection_adapters/postgresql/oid/cidr.rb b/activerecord/lib/active_record/connection_adapters/postgresql/oid/cidr.rb index eeccb09bdf..5225609e37 100644 --- a/activerecord/lib/active_record/connection_adapters/postgresql/oid/cidr.rb +++ b/activerecord/lib/active_record/connection_adapters/postgresql/oid/cidr.rb @@ -1,3 +1,5 @@ +require "ipaddr" + module ActiveRecord module ConnectionAdapters module PostgreSQL diff --git a/activerecord/lib/active_record/connection_adapters/postgresql/oid/date_time.rb b/activerecord/lib/active_record/connection_adapters/postgresql/oid/date_time.rb index 2c04c46131..b7acbf7178 100644 --- a/activerecord/lib/active_record/connection_adapters/postgresql/oid/date_time.rb +++ b/activerecord/lib/active_record/connection_adapters/postgresql/oid/date_time.rb @@ -4,18 +4,14 @@ module ActiveRecord module OID # :nodoc: class DateTime < Type::DateTime # :nodoc: def cast_value(value) - if value.is_a?(::String) - case value - when 'infinity' then ::Float::INFINITY - when '-infinity' then -::Float::INFINITY - when / BC$/ - astronomical_year = format("%04d", -value[/^\d+/].to_i + 1) - super(value.sub(/ BC$/, "").sub(/^\d+/, astronomical_year)) - else - super - end + case value + when "infinity" then ::Float::INFINITY + when "-infinity" then -::Float::INFINITY + when / BC$/ + astronomical_year = format("%04d", -value[/^\d+/].to_i + 1) + super(value.sub(/ BC$/, "").sub(/^\d+/, astronomical_year)) else - value + super end end end diff --git a/activerecord/lib/active_record/connection_adapters/postgresql/oid/enum.rb b/activerecord/lib/active_record/connection_adapters/postgresql/oid/enum.rb index 91d339f32c..950d23d516 100644 --- a/activerecord/lib/active_record/connection_adapters/postgresql/oid/enum.rb +++ b/activerecord/lib/active_record/connection_adapters/postgresql/oid/enum.rb @@ -9,9 +9,9 @@ module ActiveRecord private - def cast_value(value) - value.to_s - end + def cast_value(value) + value.to_s + end end end end diff --git a/activerecord/lib/active_record/connection_adapters/postgresql/oid/hstore.rb b/activerecord/lib/active_record/connection_adapters/postgresql/oid/hstore.rb index 9270fc9f21..d629ebca91 100644 --- a/activerecord/lib/active_record/connection_adapters/postgresql/oid/hstore.rb +++ b/activerecord/lib/active_record/connection_adapters/postgresql/oid/hstore.rb @@ -12,8 +12,8 @@ module ActiveRecord def deserialize(value) if value.is_a?(::String) ::Hash[value.scan(HstorePair).map { |k, v| - v = v.upcase == 'NULL' ? nil : v.gsub(/\A"(.*)"\Z/m,'\1').gsub(/\\(.)/, '\1') - k = k.gsub(/\A"(.*)"\Z/m,'\1').gsub(/\\(.)/, '\1') + v = v.upcase == "NULL" ? nil : v.gsub(/\A"(.*)"\Z/m, '\1').gsub(/\\(.)/, '\1') + k = k.gsub(/\A"(.*)"\Z/m, '\1').gsub(/\\(.)/, '\1') [k, v] }] else @@ -23,7 +23,9 @@ module ActiveRecord def serialize(value) if value.is_a?(::Hash) - value.map { |k, v| "#{escape_hstore(k)}=>#{escape_hstore(v)}" }.join(', ') + value.map { |k, v| "#{escape_hstore(k)}=>#{escape_hstore(v)}" }.join(", ") + elsif value.respond_to?(:to_unsafe_h) + serialize(value.to_unsafe_h) else value end @@ -35,23 +37,23 @@ module ActiveRecord private - HstorePair = begin - quoted_string = /"[^"\\]*(?:\\.[^"\\]*)*"/ - unquoted_string = /(?:\\.|[^\s,])[^\s=,\\]*(?:\\.[^\s=,\\]*|=[^,>])*/ - /(#{quoted_string}|#{unquoted_string})\s*=>\s*(#{quoted_string}|#{unquoted_string})/ - end + HstorePair = begin + quoted_string = /"[^"\\]*(?:\\.[^"\\]*)*"/ + unquoted_string = /(?:\\.|[^\s,])[^\s=,\\]*(?:\\.[^\s=,\\]*|=[^,>])*/ + /(#{quoted_string}|#{unquoted_string})\s*=>\s*(#{quoted_string}|#{unquoted_string})/ + end - def escape_hstore(value) - if value.nil? - 'NULL' - else - if value == "" - '""' + def escape_hstore(value) + if value.nil? + "NULL" else - '"%s"' % value.to_s.gsub(/(["\\])/, '\\\\\1') + if value == "" + '""' + else + '"%s"' % value.to_s.gsub(/(["\\])/, '\\\\\1') + end end end - end end end end diff --git a/activerecord/lib/active_record/connection_adapters/postgresql/oid/json.rb b/activerecord/lib/active_record/connection_adapters/postgresql/oid/json.rb index 8e1256baad..dbc879ffd4 100644 --- a/activerecord/lib/active_record/connection_adapters/postgresql/oid/json.rb +++ b/activerecord/lib/active_record/connection_adapters/postgresql/oid/json.rb @@ -2,32 +2,7 @@ module ActiveRecord module ConnectionAdapters module PostgreSQL module OID # :nodoc: - class Json < Type::Value # :nodoc: - include Type::Helpers::Mutable - - def type - :json - end - - def deserialize(value) - if value.is_a?(::String) - ::ActiveSupport::JSON.decode(value) rescue nil - else - value - end - end - - def serialize(value) - if value.is_a?(::Array) || value.is_a?(::Hash) - ::ActiveSupport::JSON.encode(value) - else - value - end - end - - def accessor - ActiveRecord::Store::StringKeyedHashAccessor - end + class Json < Type::Internal::AbstractJson end end end diff --git a/activerecord/lib/active_record/connection_adapters/postgresql/oid/rails_5_1_point.rb b/activerecord/lib/active_record/connection_adapters/postgresql/oid/legacy_point.rb index 7427a25ad5..775eecaf85 100644 --- a/activerecord/lib/active_record/connection_adapters/postgresql/oid/rails_5_1_point.rb +++ b/activerecord/lib/active_record/connection_adapters/postgresql/oid/legacy_point.rb @@ -1,10 +1,8 @@ module ActiveRecord - Point = Struct.new(:x, :y) - module ConnectionAdapters module PostgreSQL module OID # :nodoc: - class Rails51Point < Type::Value # :nodoc: + class LegacyPoint < Type::Value # :nodoc: include Type::Helpers::Mutable def type @@ -14,21 +12,20 @@ module ActiveRecord def cast(value) case value when ::String - if value[0] == '(' && value[-1] == ')' + if value[0] == "(" && value[-1] == ")" value = value[1...-1] end - x, y = value.split(",") - build_point(x, y) + cast(value.split(",")) when ::Array - build_point(*value) + value.map { |v| Float(v) } else value end end def serialize(value) - if value.is_a?(ActiveRecord::Point) - "(#{number_for_point(value.x)},#{number_for_point(value.y)})" + if value.is_a?(::Array) + "(#{number_for_point(value[0])},#{number_for_point(value[1])})" else super end @@ -36,13 +33,9 @@ module ActiveRecord private - def number_for_point(number) - number.to_s.gsub(/\.0$/, '') - end - - def build_point(x, y) - ActiveRecord::Point.new(Float(x), Float(y)) - end + def number_for_point(number) + number.to_s.gsub(/\.0$/, "") + end end end end diff --git a/activerecord/lib/active_record/connection_adapters/postgresql/oid/money.rb b/activerecord/lib/active_record/connection_adapters/postgresql/oid/money.rb index 2163674019..7a91272d1c 100644 --- a/activerecord/lib/active_record/connection_adapters/postgresql/oid/money.rb +++ b/activerecord/lib/active_record/connection_adapters/postgresql/oid/money.rb @@ -3,8 +3,6 @@ module ActiveRecord module PostgreSQL module OID # :nodoc: class Money < Type::Decimal # :nodoc: - class_attribute :precision - def type :money end @@ -27,9 +25,9 @@ module ActiveRecord value.sub!(/^\((.+)\)$/, '-\1') # (4) case value when /^-?\D+[\d,]+\.\d{2}$/ # (1) - value.gsub!(/[^-\d.]/, '') + value.gsub!(/[^-\d.]/, "") when /^-?\D+[\d.]+,\d{2}$/ # (2) - value.gsub!(/[^-\d,]/, '').sub!(/,/, '.') + value.gsub!(/[^-\d,]/, "").sub!(/,/, ".") end super(value) diff --git a/activerecord/lib/active_record/connection_adapters/postgresql/oid/point.rb b/activerecord/lib/active_record/connection_adapters/postgresql/oid/point.rb index bf565bcf47..7c764e7287 100644 --- a/activerecord/lib/active_record/connection_adapters/postgresql/oid/point.rb +++ b/activerecord/lib/active_record/connection_adapters/postgresql/oid/point.rb @@ -1,4 +1,6 @@ module ActiveRecord + Point = Struct.new(:x, :y) + module ConnectionAdapters module PostgreSQL module OID # :nodoc: @@ -12,20 +14,34 @@ module ActiveRecord def cast(value) case value when ::String - if value[0] == '(' && value[-1] == ')' + return if value.blank? + + if value[0] == "(" && value[-1] == ")" value = value[1...-1] end - cast(value.split(',')) + x, y = value.split(",") + build_point(x, y) when ::Array - value.map { |v| Float(v) } + build_point(*value) else value end end def serialize(value) - if value.is_a?(::Array) - "(#{number_for_point(value[0])},#{number_for_point(value[1])})" + case value + when ActiveRecord::Point + "(#{number_for_point(value.x)},#{number_for_point(value.y)})" + when ::Array + serialize(build_point(*value)) + else + super + end + end + + def type_cast_for_schema(value) + if ActiveRecord::Point === value + [value.x, value.y] else super end @@ -33,9 +49,13 @@ module ActiveRecord private - def number_for_point(number) - number.to_s.gsub(/\.0$/, '') - end + def number_for_point(number) + number.to_s.gsub(/\.0$/, "") + end + + def build_point(x, y) + ActiveRecord::Point.new(Float(x), Float(y)) + end end end end diff --git a/activerecord/lib/active_record/connection_adapters/postgresql/oid/range.rb b/activerecord/lib/active_record/connection_adapters/postgresql/oid/range.rb index fc201f8fb9..2c714f4018 100644 --- a/activerecord/lib/active_record/connection_adapters/postgresql/oid/range.rb +++ b/activerecord/lib/active_record/connection_adapters/postgresql/oid/range.rb @@ -1,4 +1,4 @@ -require 'active_support/core_ext/string/filters' +require "active_support/core_ext/string/filters" module ActiveRecord module ConnectionAdapters @@ -6,6 +6,7 @@ module ActiveRecord module OID # :nodoc: class Range < Type::Value # :nodoc: attr_reader :subtype, :type + delegate :user_input_in_time_zone, to: :subtype def initialize(subtype, type = :range) @subtype = subtype @@ -13,12 +14,12 @@ module ActiveRecord end def type_cast_for_schema(value) - value.inspect.gsub('Infinity', '::Float::INFINITY') + value.inspect.gsub("Infinity", "::Float::INFINITY") end def cast_value(value) - return if value == 'empty' - return value if value.is_a?(::Range) + return if value == "empty" + return value unless value.is_a?(::String) extracted = extract_bounds(value) from = type_cast_single extracted[:from] @@ -46,39 +47,45 @@ module ActiveRecord other.type == type end + def map(value) # :nodoc: + new_begin = yield(value.begin) + new_end = yield(value.end) + ::Range.new(new_begin, new_end, value.exclude_end?) + end + private - def type_cast_single(value) - infinity?(value) ? value : @subtype.deserialize(value) - end + def type_cast_single(value) + infinity?(value) ? value : @subtype.deserialize(value) + end - def type_cast_single_for_database(value) - infinity?(value) ? '' : @subtype.serialize(value) - end + def type_cast_single_for_database(value) + infinity?(value) ? "" : @subtype.serialize(value) + end - def extract_bounds(value) - from, to = value[1..-2].split(',') - { - from: (value[1] == ',' || from == '-infinity') ? infinity(negative: true) : from, - to: (value[-2] == ',' || to == 'infinity') ? infinity : to, - exclude_start: (value[0] == '('), - exclude_end: (value[-1] == ')') - } - end + def extract_bounds(value) + from, to = value[1..-2].split(",") + { + from: (value[1] == "," || from == "-infinity") ? infinity(negative: true) : from, + to: (value[-2] == "," || to == "infinity") ? infinity : to, + exclude_start: (value[0] == "("), + exclude_end: (value[-1] == ")") + } + end - def infinity(negative: false) - if subtype.respond_to?(:infinity) - subtype.infinity(negative: negative) - elsif negative - -::Float::INFINITY - else - ::Float::INFINITY + def infinity(negative: false) + if subtype.respond_to?(:infinity) + subtype.infinity(negative: negative) + elsif negative + -::Float::INFINITY + else + ::Float::INFINITY + end end - end - def infinity?(value) - value.respond_to?(:infinite?) && value.infinite? - end + def infinity?(value) + value.respond_to?(:infinite?) && value.infinite? + end end end end diff --git a/activerecord/lib/active_record/connection_adapters/postgresql/oid/type_map_initializer.rb b/activerecord/lib/active_record/connection_adapters/postgresql/oid/type_map_initializer.rb index 191c828e60..d9ae1aa7a2 100644 --- a/activerecord/lib/active_record/connection_adapters/postgresql/oid/type_map_initializer.rb +++ b/activerecord/lib/active_record/connection_adapters/postgresql/oid/type_map_initializer.rb @@ -13,13 +13,13 @@ module ActiveRecord end def run(records) - nodes = records.reject { |row| @store.key? row['oid'].to_i } - mapped, nodes = nodes.partition { |row| @store.key? row['typname'] } - ranges, nodes = nodes.partition { |row| row['typtype'] == 'r'.freeze } - enums, nodes = nodes.partition { |row| row['typtype'] == 'e'.freeze } - domains, nodes = nodes.partition { |row| row['typtype'] == 'd'.freeze } - arrays, nodes = nodes.partition { |row| row['typinput'] == 'array_in'.freeze } - composites, nodes = nodes.partition { |row| row['typelem'].to_i != 0 } + nodes = records.reject { |row| @store.key? row["oid"].to_i } + mapped, nodes = nodes.partition { |row| @store.key? row["typname"] } + ranges, nodes = nodes.partition { |row| row["typtype"] == "r".freeze } + enums, nodes = nodes.partition { |row| row["typtype"] == "e".freeze } + domains, nodes = nodes.partition { |row| row["typtype"] == "d".freeze } + arrays, nodes = nodes.partition { |row| row["typinput"] == "array_in".freeze } + composites, nodes = nodes.partition { |row| row["typelem"].to_i != 0 } mapped.each { |row| register_mapped_type(row) } enums.each { |row| register_enum_type(row) } @@ -36,72 +36,72 @@ module ActiveRecord WHERE t.typname IN (%s) OR t.typtype IN (%s) - OR t.typinput::varchar = 'array_in' + OR t.typinput = 'array_in(cstring,oid,integer)'::regprocedure OR t.typelem != 0 SQL end private - def register_mapped_type(row) - alias_type row['oid'], row['typname'] - end + def register_mapped_type(row) + alias_type row["oid"], row["typname"] + end - def register_enum_type(row) - register row['oid'], OID::Enum.new - end + def register_enum_type(row) + register row["oid"], OID::Enum.new + end - def register_array_type(row) - register_with_subtype(row['oid'], row['typelem'].to_i) do |subtype| - OID::Array.new(subtype, row['typdelim']) + def register_array_type(row) + register_with_subtype(row["oid"], row["typelem"].to_i) do |subtype| + OID::Array.new(subtype, row["typdelim"]) + end end - end - def register_range_type(row) - register_with_subtype(row['oid'], row['rngsubtype'].to_i) do |subtype| - OID::Range.new(subtype, row['typname'].to_sym) + def register_range_type(row) + register_with_subtype(row["oid"], row["rngsubtype"].to_i) do |subtype| + OID::Range.new(subtype, row["typname"].to_sym) + end end - end - def register_domain_type(row) - if base_type = @store.lookup(row["typbasetype"].to_i) - register row['oid'], base_type - else - warn "unknown base type (OID: #{row["typbasetype"]}) for domain #{row["typname"]}." + def register_domain_type(row) + if base_type = @store.lookup(row["typbasetype"].to_i) + register row["oid"], base_type + else + warn "unknown base type (OID: #{row["typbasetype"]}) for domain #{row["typname"]}." + end end - end - def register_composite_type(row) - if subtype = @store.lookup(row['typelem'].to_i) - register row['oid'], OID::Vector.new(row['typdelim'], subtype) + def register_composite_type(row) + if subtype = @store.lookup(row["typelem"].to_i) + register row["oid"], OID::Vector.new(row["typdelim"], subtype) + end end - end - def register(oid, oid_type = nil, &block) - oid = assert_valid_registration(oid, oid_type || block) - if block_given? - @store.register_type(oid, &block) - else - @store.register_type(oid, oid_type) + def register(oid, oid_type = nil, &block) + oid = assert_valid_registration(oid, oid_type || block) + if block_given? + @store.register_type(oid, &block) + else + @store.register_type(oid, oid_type) + end end - end - def alias_type(oid, target) - oid = assert_valid_registration(oid, target) - @store.alias_type(oid, target) - end + def alias_type(oid, target) + oid = assert_valid_registration(oid, target) + @store.alias_type(oid, target) + end - def register_with_subtype(oid, target_oid) - if @store.key?(target_oid) - register(oid) do |_, *args| - yield @store.lookup(target_oid, *args) + def register_with_subtype(oid, target_oid) + if @store.key?(target_oid) + register(oid) do |_, *args| + yield @store.lookup(target_oid, *args) + end end end - end - def assert_valid_registration(oid, oid_type) - raise ArgumentError, "can't register nil type for OID #{oid}" if oid_type.nil? - oid.to_i - end + def assert_valid_registration(oid, oid_type) + raise ArgumentError, "can't register nil type for OID #{oid}" if oid_type.nil? + oid.to_i + end end end end diff --git a/activerecord/lib/active_record/connection_adapters/postgresql/quoting.rb b/activerecord/lib/active_record/connection_adapters/postgresql/quoting.rb index f175730551..b5031d890f 100644 --- a/activerecord/lib/active_record/connection_adapters/postgresql/quoting.rb +++ b/activerecord/lib/active_record/connection_adapters/postgresql/quoting.rb @@ -27,8 +27,13 @@ module ActiveRecord # - schema_name."table.name" # - "schema.name".table_name # - "schema.name"."table.name" - def quote_table_name(name) - Utils.extract_schema_qualified_name(name.to_s).quoted + def quote_table_name(name) # :nodoc: + @quoted_table_names[name] ||= Utils.extract_schema_qualified_name(name.to_s).quoted.freeze + end + + # Quotes schema names for use in SQL queries. + def quote_schema_name(name) + PGconn.quote_ident(name) end def quote_table_name_for_assignment(table, attr) @@ -36,8 +41,8 @@ module ActiveRecord end # Quotes column names for use in SQL queries. - def quote_column_name(name) #:nodoc: - PGconn.quote_ident(name.to_s) + def quote_column_name(name) # :nodoc: + @quoted_column_names[name] ||= PGconn.quote_ident(super).freeze end # Quote date/time values for use in SQL input. @@ -50,10 +55,11 @@ module ActiveRecord end end - # Does not quote function default values for UUID columns - def quote_default_expression(value, column) #:nodoc: - if column.type == :uuid && value =~ /\(\)/ - value + def quote_default_expression(value, column) # :nodoc: + if value.is_a?(Proc) + value.call + elsif column.type == :uuid && value.include?("()") + value # Does not quote function default values for UUID columns elsif column.respond_to?(:array?) value = type_cast_from_column(column, value) quote(value) @@ -68,42 +74,42 @@ module ActiveRecord private - def _quote(value) - case value - when Type::Binary::Data - "'#{escape_bytea(value.to_s)}'" - when OID::Xml::Data - "xml '#{quote_string(value.to_s)}'" - when OID::Bit::Data - if value.binary? - "B'#{value}'" - elsif value.hex? - "X'#{value}'" - end - when Float - if value.infinite? || value.nan? - "'#{value}'" + def _quote(value) + case value + when Type::Binary::Data + "'#{escape_bytea(value.to_s)}'" + when OID::Xml::Data + "xml '#{quote_string(value.to_s)}'" + when OID::Bit::Data + if value.binary? + "B'#{value}'" + elsif value.hex? + "X'#{value}'" + end + when Float + if value.infinite? || value.nan? + "'#{value}'" + else + super + end else super end - else - super end - end - def _type_cast(value) - case value - when Type::Binary::Data - # Return a bind param hash with format as binary. - # See http://deveiate.org/code/pg/PGconn.html#method-i-exec_prepared-doc - # for more information - { value: value.to_s, format: 1 } - when OID::Xml::Data, OID::Bit::Data - value.to_s - else - super + def _type_cast(value) + case value + when Type::Binary::Data + # Return a bind param hash with format as binary. + # See http://deveiate.org/code/pg/PGconn.html#method-i-exec_prepared-doc + # for more information + { value: value.to_s, format: 1 } + when OID::Xml::Data, OID::Bit::Data + value.to_s + else + super + end end - end end end end diff --git a/activerecord/lib/active_record/connection_adapters/postgresql/schema_definitions.rb b/activerecord/lib/active_record/connection_adapters/postgresql/schema_definitions.rb index 022dbdfa27..4afb4733eb 100644 --- a/activerecord/lib/active_record/connection_adapters/postgresql/schema_definitions.rb +++ b/activerecord/lib/active_record/connection_adapters/postgresql/schema_definitions.rb @@ -11,11 +11,22 @@ module ActiveRecord # t.timestamps # end # - # By default, this will use the +uuid_generate_v4()+ function from the - # +uuid-ossp+ extension, which MUST be enabled on your database. To enable - # the +uuid-ossp+ extension, you can use the +enable_extension+ method in your - # migrations. To use a UUID primary key without +uuid-ossp+ enabled, you can - # set the +:default+ option to +nil+: + # By default, this will use the +gen_random_uuid()+ function from the + # +pgcrypto+ extension. As that extension is only available in + # PostgreSQL 9.4+, for earlier versions an explicit default can be set + # to use +uuid_generate_v4()+ from the +uuid-ossp+ extension instead: + # + # create_table :stuffs, id: false do |t| + # t.primary_key :id, :uuid, default: "uuid_generate_v4()" + # t.uuid :foo_id + # t.timestamps + # end + # + # To enable the appropriate extension, which is a requirement, use + # the +enable_extension+ method in your migrations. + # + # To use a UUID primary key without any of the extensions, set the + # +:default+ option to +nil+: # # create_table :stuffs, id: false do |t| # t.primary_key :id, :uuid, default: nil @@ -23,15 +34,24 @@ module ActiveRecord # t.timestamps # end # - # You may also pass a different UUID generation function from +uuid-ossp+ - # or another library. + # You may also pass a custom stored procedure that returns a UUID or use a + # different UUID generation function from another library. # # Note that setting the UUID primary key default value to +nil+ will # require you to assure that you always provide a UUID value before saving # a record (as primary keys cannot be +nil+). This might be done via the # +SecureRandom.uuid+ method and a +before_save+ callback, for instance. def primary_key(name, type = :primary_key, **options) - options[:default] = options.fetch(:default, 'uuid_generate_v4()') if type == :uuid + if type == :uuid + options[:default] = options.fetch(:default, "gen_random_uuid()") + elsif options.delete(:auto_increment) == true && %i(integer bigint).include?(type) + type = if type == :bigint || options[:limit] == 8 + :bigserial + else + :serial + end + end + super end @@ -103,6 +123,30 @@ module ActiveRecord args.each { |name| column(name, :point, options) } end + def line(*args, **options) + args.each { |name| column(name, :line, options) } + end + + def lseg(*args, **options) + args.each { |name| column(name, :lseg, options) } + end + + def box(*args, **options) + args.each { |name| column(name, :box, options) } + end + + def path(*args, **options) + args.each { |name| column(name, :path, options) } + end + + def polygon(*args, **options) + args.each { |name| column(name, :polygon, options) } + end + + def circle(*args, **options) + args.each { |name| column(name, :circle, options) } + end + def serial(*args, **options) args.each { |name| column(name, :serial, options) } end diff --git a/activerecord/lib/active_record/connection_adapters/postgresql/schema_dumper.rb b/activerecord/lib/active_record/connection_adapters/postgresql/schema_dumper.rb new file mode 100644 index 0000000000..7808d37deb --- /dev/null +++ b/activerecord/lib/active_record/connection_adapters/postgresql/schema_dumper.rb @@ -0,0 +1,47 @@ +module ActiveRecord + module ConnectionAdapters + module PostgreSQL + module ColumnDumper + def column_spec_for_primary_key(column) + spec = super + if schema_type(column) == :uuid + spec[:default] ||= "nil" + end + spec + end + + # Adds +:array+ option to the default set + def prepare_column_options(column) + spec = super + spec[:array] = "true" if column.array? + spec + end + + # Adds +:array+ as a valid migration key + def migration_keys + super + [:array] + end + + private + + def default_primary_key?(column) + schema_type(column) == :bigserial + end + + def schema_type(column) + return super unless column.serial? + + if column.bigint? + :bigserial + else + :serial + end + end + + def schema_expression(column) + super unless column.serial? + end + end + end + end +end diff --git a/activerecord/lib/active_record/connection_adapters/postgresql/schema_statements.rb b/activerecord/lib/active_record/connection_adapters/postgresql/schema_statements.rb index d114cad16b..85836fc575 100644 --- a/activerecord/lib/active_record/connection_adapters/postgresql/schema_statements.rb +++ b/activerecord/lib/active_record/connection_adapters/postgresql/schema_statements.rb @@ -1,20 +1,22 @@ +require "active_support/core_ext/string/strip" + module ActiveRecord module ConnectionAdapters module PostgreSQL class SchemaCreation < AbstractAdapter::SchemaCreation private - def visit_ColumnDefinition(o) - o.sql_type = type_to_sql(o.type, o.limit, o.precision, o.scale, o.array) - super - end + def visit_ColumnDefinition(o) + o.sql_type = type_to_sql(o.type, o.limit, o.precision, o.scale, o.array) + super + end - def add_column_options!(sql, options) - if options[:collation] - sql << " COLLATE \"#{options[:collation]}\"" + def add_column_options!(sql, options) + if options[:collation] + sql << " COLLATE \"#{options[:collation]}\"" + end + super end - super - end end module SchemaStatements @@ -34,26 +36,26 @@ module ActiveRecord # create_database config[:database], config # create_database 'foo_development', encoding: 'unicode' def create_database(name, options = {}) - options = { encoding: 'utf8' }.merge!(options.symbolize_keys) + options = { encoding: "utf8" }.merge!(options.symbolize_keys) option_string = options.inject("") do |memo, (key, value)| memo += case key - when :owner - " OWNER = \"#{value}\"" - when :template - " TEMPLATE = \"#{value}\"" - when :encoding - " ENCODING = '#{value}'" - when :collation - " LC_COLLATE = '#{value}'" - when :ctype - " LC_CTYPE = '#{value}'" - when :tablespace - " TABLESPACE = \"#{value}\"" - when :connection_limit - " CONNECTION LIMIT = #{value}" + when :owner + " OWNER = \"#{value}\"" + when :template + " TEMPLATE = \"#{value}\"" + when :encoding + " ENCODING = '#{value}'" + when :collation + " LC_COLLATE = '#{value}'" + when :ctype + " LC_CTYPE = '#{value}'" + when :tablespace + " TABLESPACE = \"#{value}\"" + when :connection_limit + " CONNECTION LIMIT = #{value}" else - "" + "" end end @@ -68,9 +70,29 @@ module ActiveRecord execute "DROP DATABASE IF EXISTS #{quote_table_name(name)}" end - # Returns the list of all tables in the schema search path or a specified schema. - def tables(name = nil) - select_values("SELECT tablename FROM pg_tables WHERE schemaname = ANY(current_schemas(false))", 'SCHEMA') + # Returns the list of all tables in the schema search path. + def tables + select_values("SELECT tablename FROM pg_tables WHERE schemaname = ANY(current_schemas(false))", "SCHEMA") + end + + def data_sources # :nodoc + select_values(<<-SQL, "SCHEMA") + SELECT c.relname + FROM pg_class c + LEFT JOIN pg_namespace n ON n.oid = c.relnamespace + WHERE c.relkind IN ('r','v','m') -- (r)elation/table, (v)iew, (m)aterialized view + AND n.nspname = ANY (current_schemas(false)) + SQL + end + + def views # :nodoc: + select_values(<<-SQL, "SCHEMA") + SELECT c.relname + FROM pg_class c + LEFT JOIN pg_namespace n ON n.oid = c.relnamespace + WHERE c.relkind IN ('v','m') -- (v)iew, (m)aterialized view + AND n.nspname = ANY (current_schemas(false)) + SQL end # Returns true if table exists. @@ -80,13 +102,39 @@ module ActiveRecord name = Utils.extract_schema_qualified_name(name.to_s) return false unless name.identifier - select_value(<<-SQL, 'SCHEMA').to_i > 0 - SELECT COUNT(*) - FROM pg_class c - LEFT JOIN pg_namespace n ON n.oid = c.relnamespace - WHERE c.relkind IN ('r','v','m') -- (r)elation/table, (v)iew, (m)aterialized view - AND c.relname = '#{name.identifier}' - AND n.nspname = #{name.schema ? "'#{name.schema}'" : 'ANY (current_schemas(false))'} + select_values(<<-SQL, "SCHEMA").any? + SELECT tablename + FROM pg_tables + WHERE tablename = #{quote(name.identifier)} + AND schemaname = #{name.schema ? quote(name.schema) : "ANY (current_schemas(false))"} + SQL + end + + def data_source_exists?(name) # :nodoc: + name = Utils.extract_schema_qualified_name(name.to_s) + return false unless name.identifier + + select_values(<<-SQL, "SCHEMA").any? + SELECT c.relname + FROM pg_class c + LEFT JOIN pg_namespace n ON n.oid = c.relnamespace + WHERE c.relkind IN ('r','v','m') -- (r)elation/table, (v)iew, (m)aterialized view + AND c.relname = #{quote(name.identifier)} + AND n.nspname = #{name.schema ? quote(name.schema) : "ANY (current_schemas(false))"} + SQL + end + + def view_exists?(view_name) # :nodoc: + name = Utils.extract_schema_qualified_name(view_name.to_s) + return false unless name.identifier + + select_values(<<-SQL, "SCHEMA").any? + SELECT c.relname + FROM pg_class c + LEFT JOIN pg_namespace n ON n.oid = c.relnamespace + WHERE c.relkind IN ('v','m') -- (v)iew, (m)aterialized view + AND c.relname = #{quote(name.identifier)} + AND n.nspname = #{name.schema ? quote(name.schema) : "ANY (current_schemas(false))"} SQL end @@ -96,34 +144,45 @@ module ActiveRecord # Returns true if schema exists. def schema_exists?(name) - select_value("SELECT COUNT(*) FROM pg_namespace WHERE nspname = '#{name}'", 'SCHEMA').to_i > 0 + select_value("SELECT COUNT(*) FROM pg_namespace WHERE nspname = '#{name}'", "SCHEMA").to_i > 0 end # Verifies existence of an index with a given name. def index_name_exists?(table_name, index_name, default) - select_value(<<-SQL, 'SCHEMA').to_i > 0 + table = Utils.extract_schema_qualified_name(table_name.to_s) + index = Utils.extract_schema_qualified_name(index_name.to_s) + + select_value(<<-SQL, "SCHEMA").to_i > 0 SELECT COUNT(*) FROM pg_class t INNER JOIN pg_index d ON t.oid = d.indrelid INNER JOIN pg_class i ON d.indexrelid = i.oid + LEFT JOIN pg_namespace n ON n.oid = i.relnamespace WHERE i.relkind = 'i' - AND i.relname = '#{index_name}' - AND t.relname = '#{table_name}' - AND i.relnamespace IN (SELECT oid FROM pg_namespace WHERE nspname = ANY (current_schemas(false)) ) + AND i.relname = '#{index.identifier}' + AND t.relname = '#{table.identifier}' + AND n.nspname = #{index.schema ? "'#{index.schema}'" : 'ANY (current_schemas(false))'} SQL end # Returns an array of indexes for the given table. def indexes(table_name, name = nil) - result = query(<<-SQL, 'SCHEMA') - SELECT distinct i.relname, d.indisunique, d.indkey, pg_get_indexdef(d.indexrelid), t.oid - FROM pg_class t - INNER JOIN pg_index d ON t.oid = d.indrelid - INNER JOIN pg_class i ON d.indexrelid = i.oid - WHERE i.relkind = 'i' - AND d.indisprimary = 'f' - AND t.relname = '#{table_name}' - AND i.relnamespace IN (SELECT oid FROM pg_namespace WHERE nspname = ANY (current_schemas(false)) ) + table = Utils.extract_schema_qualified_name(table_name.to_s) + + result = query(<<-SQL, "SCHEMA") + SELECT distinct i.relname, d.indisunique, d.indkey, pg_get_indexdef(d.indexrelid), t.oid, + pg_catalog.obj_description(i.oid, 'pg_class') AS comment, + (SELECT COUNT(*) FROM pg_opclass o + JOIN (SELECT unnest(string_to_array(d.indclass::text, ' '))::int oid) c + ON o.oid = c.oid WHERE o.opcdefault = 'f') + FROM pg_class t + INNER JOIN pg_index d ON t.oid = d.indrelid + INNER JOIN pg_class i ON d.indexrelid = i.oid + LEFT JOIN pg_namespace n ON n.oid = i.relnamespace + WHERE i.relkind = 'i' + AND d.indisprimary = 'f' + AND t.relname = '#{table.identifier}' + AND n.nspname = #{table.schema ? "'#{table.schema}'" : 'ANY (current_schemas(false))'} ORDER BY i.relname SQL @@ -133,73 +192,99 @@ module ActiveRecord indkey = row[2].split(" ").map(&:to_i) inddef = row[3] oid = row[4] + comment = row[5] + opclass = row[6] - columns = Hash[query(<<-SQL, "SCHEMA")] - SELECT a.attnum, a.attname - FROM pg_attribute a - WHERE a.attrelid = #{oid} - AND a.attnum IN (#{indkey.join(",")}) - SQL + using, expressions, where = inddef.scan(/ USING (\w+?) \((.+?)\)(?: WHERE (.+))?\z/).flatten - column_names = columns.values_at(*indkey).compact + if indkey.include?(0) || opclass > 0 + columns = expressions + else + columns = Hash[query(<<-SQL.strip_heredoc, "SCHEMA")].values_at(*indkey).compact + SELECT a.attnum, a.attname + FROM pg_attribute a + WHERE a.attrelid = #{oid} + AND a.attnum IN (#{indkey.join(",")}) + SQL - unless column_names.empty? # add info on sort order for columns (only desc order is explicitly specified, asc is the default) - desc_order_columns = inddef.scan(/(\w+) DESC/).flatten - orders = desc_order_columns.any? ? Hash[desc_order_columns.map {|order_column| [order_column, :desc]}] : {} - where = inddef.scan(/WHERE (.+)$/).flatten[0] - using = inddef.scan(/USING (.+?) /).flatten[0].to_sym - - IndexDefinition.new(table_name, index_name, unique, column_names, [], orders, where, nil, using) + orders = Hash[ + expressions.scan(/(\w+) DESC/).flatten.map { |order_column| [order_column, :desc] } + ] end + + IndexDefinition.new(table_name, index_name, unique, columns, [], orders, where, nil, using.to_sym, comment.presence) end.compact end - # Returns the list of all column definitions for a table. - def columns(table_name) - # Limit, precision, and scale are all handled by the superclass. - column_definitions(table_name).map do |column_name, type, default, notnull, oid, fmod, collation| - oid = oid.to_i - fmod = fmod.to_i - type_metadata = fetch_type_metadata(column_name, type, oid, fmod) - default_value = extract_value_from_default(default) - default_function = extract_default_function(default_value, default) - new_column(column_name, default_value, type_metadata, !notnull, default_function, collation) + def new_column_from_field(table_name, field) # :nondoc: + column_name, type, default, notnull, oid, fmod, collation, comment = field + oid = oid.to_i + fmod = fmod.to_i + type_metadata = fetch_type_metadata(column_name, type, oid, fmod) + default_value = extract_value_from_default(default) + default_function = extract_default_function(default_value, default) + PostgreSQLColumn.new( + column_name, + default_value, + type_metadata, + !notnull, + table_name, + default_function, + collation, + comment: comment.presence + ) + end + + def table_options(table_name) # :nodoc: + if comment = table_comment(table_name) + { comment: comment } end end - def new_column(name, default, sql_type_metadata = nil, null = true, default_function = nil, collation = nil) # :nodoc: - PostgreSQLColumn.new(name, default, sql_type_metadata, null, default_function, collation) + # Returns a comment stored in database for given table + def table_comment(table_name) # :nodoc: + name = Utils.extract_schema_qualified_name(table_name.to_s) + if name.identifier + select_value(<<-SQL.strip_heredoc, "SCHEMA") + SELECT pg_catalog.obj_description(c.oid, 'pg_class') + FROM pg_catalog.pg_class c + LEFT JOIN pg_namespace n ON n.oid = c.relnamespace + WHERE c.relname = #{quote(name.identifier)} + AND c.relkind IN ('r') -- (r)elation/table + AND n.nspname = #{name.schema ? quote(name.schema) : 'ANY (current_schemas(false))'} + SQL + end end # Returns the current database name. def current_database - select_value('select current_database()', 'SCHEMA') + select_value("select current_database()", "SCHEMA") end # Returns the current schema name. def current_schema - select_value('SELECT current_schema', 'SCHEMA') + select_value("SELECT current_schema", "SCHEMA") end # Returns the current database encoding format. def encoding - select_value("SELECT pg_encoding_to_char(encoding) FROM pg_database WHERE datname LIKE '#{current_database}'", 'SCHEMA') + select_value("SELECT pg_encoding_to_char(encoding) FROM pg_database WHERE datname LIKE '#{current_database}'", "SCHEMA") end # Returns the current database collation. def collation - select_value("SELECT datcollate FROM pg_database WHERE datname LIKE '#{current_database}'", 'SCHEMA') + select_value("SELECT datcollate FROM pg_database WHERE datname LIKE '#{current_database}'", "SCHEMA") end # Returns the current database ctype. def ctype - select_value("SELECT datctype FROM pg_database WHERE datname LIKE '#{current_database}'", 'SCHEMA') + select_value("SELECT datctype FROM pg_database WHERE datname LIKE '#{current_database}'", "SCHEMA") end # Returns an array of schema names. def schema_names - select_values(<<-SQL, 'SCHEMA') + select_values(<<-SQL, "SCHEMA") SELECT nspname FROM pg_namespace WHERE nspname !~ '^pg_.*' @@ -209,13 +294,13 @@ module ActiveRecord end # Creates a schema for the given schema name. - def create_schema schema_name - execute "CREATE SCHEMA #{schema_name}" + def create_schema(schema_name) + execute "CREATE SCHEMA #{quote_schema_name(schema_name)}" end # Drops the schema for the given schema name. - def drop_schema schema_name - execute "DROP SCHEMA #{schema_name} CASCADE" + def drop_schema(schema_name, options = {}) + execute "DROP SCHEMA#{' IF EXISTS' if options[:if_exists]} #{quote_schema_name(schema_name)} CASCADE" end # Sets the schema search path to a string of comma-separated schema names. @@ -225,37 +310,37 @@ module ActiveRecord # This should be not be called manually but set in database.yml. def schema_search_path=(schema_csv) if schema_csv - execute("SET search_path TO #{schema_csv}", 'SCHEMA') + execute("SET search_path TO #{schema_csv}", "SCHEMA") @schema_search_path = schema_csv end end # Returns the active schema search path. def schema_search_path - @schema_search_path ||= select_value('SHOW search_path', 'SCHEMA') + @schema_search_path ||= select_value("SHOW search_path", "SCHEMA") end # Returns the current client message level. def client_min_messages - select_value('SHOW client_min_messages', 'SCHEMA') + select_value("SHOW client_min_messages", "SCHEMA") end # Set the client message level. def client_min_messages=(level) - execute("SET client_min_messages TO '#{level}'", 'SCHEMA') + execute("SET client_min_messages TO '#{level}'", "SCHEMA") end # Returns the sequence name for a table's primary key or some other specified key. - def default_sequence_name(table_name, pk = nil) #:nodoc: - result = serial_sequence(table_name, pk || 'id') + def default_sequence_name(table_name, pk = "id") #:nodoc: + result = serial_sequence(table_name, pk) return nil unless result Utils.extract_schema_qualified_name(result).to_s rescue ActiveRecord::StatementInvalid - PostgreSQL::Name.new(nil, "#{table_name}_#{pk || 'id'}_seq").to_s + PostgreSQL::Name.new(nil, "#{table_name}_#{pk}_seq").to_s end def serial_sequence(table, column) - select_value("SELECT pg_get_serial_sequence('#{table}', '#{column}')", 'SCHEMA') + select_value("SELECT pg_get_serial_sequence('#{table}', '#{column}')", "SCHEMA") end # Sets the sequence of a table's primary key to the specified value. @@ -266,16 +351,16 @@ module ActiveRecord if sequence quoted_sequence = quote_table_name(sequence) - select_value("SELECT setval('#{quoted_sequence}', #{value})", 'SCHEMA') + select_value("SELECT setval('#{quoted_sequence}', #{value})", "SCHEMA") else - @logger.warn "#{table} has primary key #{pk} with no default sequence" if @logger + @logger.warn "#{table} has primary key #{pk} with no default sequence." if @logger end end end # Resets the sequence of a table's primary key to the maximum value. def reset_pk_sequence!(table, pk = nil, sequence = nil) #:nodoc: - unless pk and sequence + unless pk && sequence default_pk, default_sequence = pk_and_sequence_for(table) pk ||= default_pk @@ -283,13 +368,13 @@ module ActiveRecord end if @logger && pk && !sequence - @logger.warn "#{table} has primary key #{pk} with no default sequence" + @logger.warn "#{table} has primary key #{pk} with no default sequence." end if pk && sequence quoted_sequence = quote_table_name(sequence) - select_value(<<-end_sql, 'SCHEMA') + select_value(<<-end_sql, "SCHEMA") SELECT setval('#{quoted_sequence}', (SELECT COALESCE(MAX(#{quote_column_name pk})+(SELECT increment_by FROM #{quoted_sequence}), (SELECT min_value FROM #{quoted_sequence})) FROM #{quote_table_name(table)}), false) end_sql end @@ -299,7 +384,7 @@ module ActiveRecord def pk_and_sequence_for(table) #:nodoc: # First try looking for a sequence with a dependency on the # given table's primary key. - result = query(<<-end_sql, 'SCHEMA')[0] + result = query(<<-end_sql, "SCHEMA")[0] SELECT attr.attname, nsp.nspname, seq.relname FROM pg_class seq, pg_attribute attr, @@ -318,8 +403,8 @@ module ActiveRecord AND dep.refobjid = '#{quote_table_name(table)}'::regclass end_sql - if result.nil? or result.empty? - result = query(<<-end_sql, 'SCHEMA')[0] + if result.nil? || result.empty? + result = query(<<-end_sql, "SCHEMA")[0] SELECT attr.attname, nsp.nspname, CASE WHEN pg_get_expr(def.adbin, def.adrelid) !~* 'nextval' THEN NULL @@ -349,17 +434,19 @@ module ActiveRecord nil end - # Returns just a table's primary key - def primary_key(table) - pks = query(<<-end_sql, 'SCHEMA') - SELECT attr.attname - FROM pg_attribute attr - INNER JOIN pg_constraint cons ON attr.attrelid = cons.conrelid AND attr.attnum = any(cons.conkey) - WHERE cons.contype = 'p' - AND cons.conrelid = '#{quote_table_name(table)}'::regclass - end_sql - return nil unless pks.count == 1 - pks[0][0] + def primary_keys(table_name) # :nodoc: + select_values(<<-SQL.strip_heredoc, "SCHEMA") + WITH pk_constraint AS ( + SELECT conrelid, unnest(conkey) AS connum FROM pg_constraint + WHERE contype = 'p' + AND conrelid = #{quote(quote_table_name(table_name))}::regclass + ), cons AS ( + SELECT conrelid, connum, row_number() OVER() AS rownum FROM pk_constraint + ) + SELECT attr.attname FROM pg_attribute attr + INNER JOIN cons ON attr.attrelid = cons.conrelid AND attr.attnum = cons.connum + ORDER BY cons.rownum + SQL end # Renames a table. @@ -376,7 +463,7 @@ module ActiveRecord new_seq = "#{new_name}_#{pk}_seq" idx = "#{table_name}_pkey" new_idx = "#{new_name}_pkey" - execute "ALTER TABLE #{quote_table_name(seq)} RENAME TO #{quote_table_name(new_seq)}" + execute "ALTER TABLE #{seq.quoted} RENAME TO #{quote_table_name(new_seq)}" execute "ALTER INDEX #{quote_table_name(idx)} RENAME TO #{quote_table_name(new_idx)}" end @@ -386,6 +473,7 @@ module ActiveRecord def add_column(table_name, column_name, type, options = {}) #:nodoc: clear_cache! super + change_column_comment(table_name, column_name, options[:comment]) if options.key?(:comment) end def change_column(table_name, column_name, type, options = {}) #:nodoc: @@ -407,6 +495,7 @@ module ActiveRecord change_column_default(table_name, column_name, options[:default]) if options_include_default?(options) change_column_null(table_name, column_name, options[:null], options[:default]) if options.key?(:null) + change_column_comment(table_name, column_name, options[:comment]) if options.key?(:comment) end # Changes the default value of a table column. @@ -435,6 +524,18 @@ module ActiveRecord execute("ALTER TABLE #{quote_table_name(table_name)} ALTER #{quote_column_name(column_name)} #{null ? 'DROP' : 'SET'} NOT NULL") end + # Adds comment for given table column or drops it if +comment+ is a +nil+ + def change_column_comment(table_name, column_name, comment) # :nodoc: + clear_cache! + execute "COMMENT ON COLUMN #{quote_table_name(table_name)}.#{quote_column_name(column_name)} IS #{quote(comment)}" + end + + # Adds comment for given table or drops it if +comment+ is a +nil+ + def change_table_comment(table_name, comment) # :nodoc: + clear_cache! + execute "COMMENT ON TABLE #{quote_table_name(table_name)} IS #{quote(comment)}" + end + # Renames a column in a table. def rename_column(table_name, column_name, new_column_name) #:nodoc: clear_cache! @@ -443,12 +544,34 @@ module ActiveRecord end def add_index(table_name, column_name, options = {}) #:nodoc: - index_name, index_type, index_columns, index_options, index_algorithm, index_using = add_index_options(table_name, column_name, options) - execute "CREATE #{index_type} INDEX #{index_algorithm} #{quote_column_name(index_name)} ON #{quote_table_name(table_name)} #{index_using} (#{index_columns})#{index_options}" + index_name, index_type, index_columns, index_options, index_algorithm, index_using, comment = add_index_options(table_name, column_name, options) + execute("CREATE #{index_type} INDEX #{index_algorithm} #{quote_column_name(index_name)} ON #{quote_table_name(table_name)} #{index_using} (#{index_columns})#{index_options}").tap do + execute "COMMENT ON INDEX #{quote_column_name(index_name)} IS #{quote(comment)}" if comment + end end - def remove_index!(table_name, index_name) #:nodoc: - execute "DROP INDEX #{quote_table_name(index_name)}" + def remove_index(table_name, options = {}) #:nodoc: + table = Utils.extract_schema_qualified_name(table_name.to_s) + + if options.is_a?(Hash) && options.key?(:name) + provided_index = Utils.extract_schema_qualified_name(options[:name].to_s) + + options[:name] = provided_index.identifier + table = PostgreSQL::Name.new(provided_index.schema, table.identifier) unless table.schema.present? + + if provided_index.schema.present? && table.schema != provided_index.schema + raise ArgumentError.new("Index schema '#{provided_index.schema}' does not match table schema '#{table.schema}'") + end + end + + index_to_remove = PostgreSQL::Name.new(table.schema, index_name_for_remove(table.to_s, options)) + algorithm = + if options.is_a?(Hash) && options.key?(:algorithm) + index_algorithms.fetch(options[:algorithm]) do + raise ArgumentError.new("Algorithm must be one of the following: #{index_algorithms.keys.map(&:inspect).join(', ')}") + end + end + execute "DROP INDEX #{algorithm} #{quote_table_name(index_to_remove)}" end # Renames an index of a table. Raises error if length of new @@ -460,7 +583,7 @@ module ActiveRecord end def foreign_keys(table_name) - fk_info = select_all <<-SQL.strip_heredoc + fk_info = select_all(<<-SQL.strip_heredoc, "SCHEMA") SELECT t2.oid::regclass::text AS to_table, a1.attname AS column, a2.attname AS primary_key, c.conname AS name, c.confupdtype AS on_update, c.confdeltype AS on_delete FROM pg_constraint c JOIN pg_class t1 ON c.conrelid = t1.oid @@ -476,23 +599,23 @@ module ActiveRecord fk_info.map do |row| options = { - column: row['column'], - name: row['name'], - primary_key: row['primary_key'] + column: row["column"], + name: row["name"], + primary_key: row["primary_key"] } - options[:on_delete] = extract_foreign_key_action(row['on_delete']) - options[:on_update] = extract_foreign_key_action(row['on_update']) + options[:on_delete] = extract_foreign_key_action(row["on_delete"]) + options[:on_update] = extract_foreign_key_action(row["on_update"]) - ForeignKeyDefinition.new(table_name, row['to_table'], options) + ForeignKeyDefinition.new(table_name, row["to_table"], options) end end def extract_foreign_key_action(specifier) # :nodoc: case specifier - when 'c'; :cascade - when 'n'; :nullify - when 'r'; :restrict + when "c"; :cascade + when "n"; :nullify + when "r"; :restrict end end @@ -502,48 +625,49 @@ module ActiveRecord # Maps logical Rails types to PostgreSQL-specific data types. def type_to_sql(type, limit = nil, precision = nil, scale = nil, array = nil) - sql = case type.to_s - when 'binary' - # PostgreSQL doesn't support limits on binary (bytea) columns. - # The hard limit is 1GB, because of a 32-bit size field, and TOAST. - case limit - when nil, 0..0x3fffffff; super(type) - else raise(ActiveRecordError, "No binary type has byte size #{limit}.") - end - when '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' - case limit - when 1, 2; 'smallint' - when nil, 3, 4; 'integer' - when 5..8; 'bigint' - else raise(ActiveRecordError, "No integer type has byte size #{limit}. Use a numeric with precision 0 instead.") + sql = \ + case type.to_s + when "binary" + # PostgreSQL doesn't support limits on binary (bytea) columns. + # The hard limit is 1GB, because of a 32-bit size field, and TOAST. + case limit + when nil, 0..0x3fffffff; super(type) + else raise(ActiveRecordError, "No binary type has byte size #{limit}.") + end + when "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" + case limit + when 1, 2; "smallint" + when nil, 3, 4; "integer" + when 5..8; "bigint" + else raise(ActiveRecordError, "No integer type has byte size #{limit}. Use a numeric with scale 0 instead.") + end + else + super(type, limit, precision, scale) end - else - super(type, limit, precision, scale) - end - sql << '[]' if array && type != :primary_key + sql << "[]" if array && type != :primary_key sql end # PostgreSQL requires the ORDER BY columns in the select list for distinct queries, and # requires that the ORDER BY include the distinct column. def columns_for_distinct(columns, orders) #:nodoc: - order_columns = orders.reject(&:blank?).map{ |s| + order_columns = orders.reject(&:blank?).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)\b/i, '') - .gsub(/\s+NULLS\s+(?:FIRST|LAST)\b/i, '') + s.gsub(/\s+(?:ASC|DESC)\b/i, "") + .gsub(/\s+NULLS\s+(?:FIRST|LAST)\b/i, "") }.reject(&:blank?).map.with_index { |column, i| "#{column} AS alias_#{i}" } - [super, *order_columns].join(', ') + [super, *order_columns].join(", ") end def fetch_type_metadata(column_name, sql_type, oid, fmod) diff --git a/activerecord/lib/active_record/connection_adapters/postgresql/type_metadata.rb b/activerecord/lib/active_record/connection_adapters/postgresql/type_metadata.rb index 58715978f7..311988625f 100644 --- a/activerecord/lib/active_record/connection_adapters/postgresql/type_metadata.rb +++ b/activerecord/lib/active_record/connection_adapters/postgresql/type_metadata.rb @@ -8,11 +8,11 @@ module ActiveRecord @type_metadata = type_metadata @oid = oid @fmod = fmod - @array = /\[\]$/ === type_metadata.sql_type + @array = /\[\]$/.match?(type_metadata.sql_type) end def sql_type - super.gsub(/\[\]$/, "") + super.gsub(/\[\]$/, "".freeze) end def ==(other) @@ -27,9 +27,9 @@ module ActiveRecord protected - def attributes_for_hash - [self.class, @type_metadata, oid, fmod] - end + def attributes_for_hash + [self.class, @type_metadata, oid, fmod] + end end end end diff --git a/activerecord/lib/active_record/connection_adapters/postgresql/utils.rb b/activerecord/lib/active_record/connection_adapters/postgresql/utils.rb index 9a0b80d7d3..a3f9ce6d64 100644 --- a/activerecord/lib/active_record/connection_adapters/postgresql/utils.rb +++ b/activerecord/lib/active_record/connection_adapters/postgresql/utils.rb @@ -35,6 +35,12 @@ module ActiveRecord end protected + + def parts + @parts ||= [@schema, @identifier].compact + end + + private def unquote(part) if part && part.start_with?('"') part[1..-2] @@ -42,10 +48,6 @@ module ActiveRecord part end end - - def parts - @parts ||= [@schema, @identifier].compact - end end module Utils # :nodoc: @@ -53,7 +55,7 @@ module ActiveRecord # Returns an instance of <tt>ActiveRecord::ConnectionAdapters::PostgreSQL::Name</tt> # extracted from +string+. - # +schema+ is nil if not specified in +string+. + # +schema+ is +nil+ if not specified in +string+. # +schema+ and +identifier+ exclude surrounding quotes (regardless of whether provided in +string+) # +string+ supports the range of schema/table references understood by PostgreSQL, for example: # diff --git a/activerecord/lib/active_record/connection_adapters/postgresql_adapter.rb b/activerecord/lib/active_record/connection_adapters/postgresql_adapter.rb index 2c43c46a3d..0ebd907cc0 100644 --- a/activerecord/lib/active_record/connection_adapters/postgresql_adapter.rb +++ b/activerecord/lib/active_record/connection_adapters/postgresql_adapter.rb @@ -1,29 +1,23 @@ # Make sure we're using pg high enough for type casts and Ruby 2.2+ compatibility -gem 'pg', '~> 0.18' -require 'pg' +gem "pg", "~> 0.18" +require "pg" require "active_record/connection_adapters/abstract_adapter" require "active_record/connection_adapters/postgresql/column" require "active_record/connection_adapters/postgresql/database_statements" +require "active_record/connection_adapters/postgresql/explain_pretty_printer" require "active_record/connection_adapters/postgresql/oid" require "active_record/connection_adapters/postgresql/quoting" require "active_record/connection_adapters/postgresql/referential_integrity" require "active_record/connection_adapters/postgresql/schema_definitions" +require "active_record/connection_adapters/postgresql/schema_dumper" require "active_record/connection_adapters/postgresql/schema_statements" require "active_record/connection_adapters/postgresql/type_metadata" require "active_record/connection_adapters/postgresql/utils" require "active_record/connection_adapters/statement_pool" -require 'ipaddr' - module ActiveRecord module ConnectionHandling # :nodoc: - VALID_CONN_PARAMS = [:host, :hostaddr, :port, :dbname, :user, :password, :connect_timeout, - :client_encoding, :options, :application_name, :fallback_application_name, - :keepalives, :keepalives_idle, :keepalives_interval, :keepalives_count, - :tty, :sslmode, :requiressl, :sslcompression, :sslcert, :sslkey, - :sslrootcert, :sslcrl, :requirepeer, :krbsrvname, :gsslib, :service] - # Establishes a connection to the database that's used by all Active Record objects def postgresql_connection(config) conn_params = config.symbolize_keys @@ -35,7 +29,8 @@ module ActiveRecord conn_params[:dbname] = conn_params.delete(:database) if conn_params[:database] # Forward only valid config params to PGconn.connect. - conn_params.keep_if { |k, _| VALID_CONN_PARAMS.include?(k) } + valid_conn_param_keys = PGconn.conndefaults_hash.keys + [:requiressl] + conn_params.slice!(*valid_conn_param_keys) # The postgres drivers don't allow the creation of an unconnected PGconn object, # so just pass a nil connection object for the time being. @@ -72,11 +67,10 @@ module ActiveRecord # In addition, default connection parameters of libpq can be set per environment variables. # See http://www.postgresql.org/docs/current/static/libpq-envars.html . class PostgreSQLAdapter < AbstractAdapter - ADAPTER_NAME = 'PostgreSQL'.freeze + ADAPTER_NAME = "PostgreSQL".freeze NATIVE_DATABASE_TYPES = { - primary_key: "serial primary key", - bigserial: "bigserial", + primary_key: "bigserial primary key", string: { name: "character varying" }, text: { name: "text" }, integer: { name: "integer" }, @@ -93,7 +87,6 @@ module ActiveRecord int8range: { name: "int8range" }, binary: { name: "bytea" }, boolean: { name: "boolean" }, - bigint: { name: "bigint" }, xml: { name: "xml" }, tsvector: { name: "tsvector" }, hstore: { name: "hstore" }, @@ -106,6 +99,12 @@ module ActiveRecord ltree: { name: "ltree" }, citext: { name: "citext" }, point: { name: "point" }, + line: { name: "line" }, + lseg: { name: "lseg" }, + box: { name: "box" }, + path: { name: "path" }, + polygon: { name: "polygon" }, + circle: { name: "circle" }, bit: { name: "bit" }, bit_varying: { name: "bit varying" }, money: { name: "money" }, @@ -117,61 +116,17 @@ module ActiveRecord include PostgreSQL::ReferentialIntegrity include PostgreSQL::SchemaStatements include PostgreSQL::DatabaseStatements - include Savepoints + include PostgreSQL::ColumnDumper def schema_creation # :nodoc: PostgreSQL::SchemaCreation.new self end - def column_spec_for_primary_key(column) - spec = {} - if column.serial? - return unless column.bigint? - spec[:id] = ':bigserial' - elsif column.type == :uuid - spec[:id] = ':uuid' - spec[:default] = column.default_function.inspect - else - spec[:id] = column.type.inspect - spec.merge!(prepare_column_options(column).delete_if { |key, _| [:name, :type, :null].include?(key) }) - end - spec - end - - # Adds +:array+ option to the default set provided by the - # AbstractAdapter - def prepare_column_options(column) # :nodoc: - spec = super - spec[:array] = 'true' if column.array? - spec + def arel_visitor # :nodoc: + Arel::Visitors::PostgreSQL.new(self) end - # Adds +:array+ as a valid migration key - def migration_keys - super + [:array] - end - - def schema_type(column) - return super unless column.serial? - - if column.bigint? - 'bigserial' - else - 'serial' - end - end - private :schema_type - - def schema_default(column) - if column.default_function - column.default_function.inspect unless column.serial? - else - super - end - end - private :schema_default - - # Returns +true+, since this connection adapter supports prepared statement + # Returns true, since this connection adapter supports prepared statement # caching. def supports_statement_cache? true @@ -185,6 +140,10 @@ module ActiveRecord true end + def supports_expression_index? + true + end + def supports_transaction_isolation? true end @@ -201,13 +160,26 @@ module ActiveRecord true end + def supports_json? + postgresql_version >= 90200 + end + + def supports_comments? + true + end + + def supports_savepoints? + true + end + def index_algorithms - { concurrently: 'CONCURRENTLY' } + { concurrently: "CONCURRENTLY" } end class StatementPool < ConnectionAdapters::StatementPool def initialize(connection, max) - super + super(max) + @connection = connection @counter = 0 end @@ -234,16 +206,9 @@ module ActiveRecord # Initializes and connects a PostgreSQL adapter. def initialize(connection, logger, connection_parameters, config) - super(connection, logger) + super(connection, logger, config) - @visitor = Arel::Visitors::PostgreSQL.new self - if self.class.type_cast_config_to_boolean(config.fetch(:prepared_statements) { true }) - @prepared_statements = true - else - @prepared_statements = false - end - - @connection_parameters, @config = connection_parameters, config + @connection_parameters = connection_parameters # @local_tz is initialized as nil to avoid warnings when connect tries to use it @local_tz = nil @@ -252,17 +217,17 @@ module ActiveRecord connect add_pg_encoders @statements = StatementPool.new @connection, - self.class.type_cast_config_to_integer(config.fetch(:statement_limit) { 1000 }) + self.class.type_cast_config_to_integer(config[:statement_limit]) - if postgresql_version < 80200 - raise "Your version of PostgreSQL (#{postgresql_version}) is too old, please upgrade!" + if postgresql_version < 90100 + raise "Your version of PostgreSQL (#{postgresql_version}) is too old. Active Record supports PostgreSQL >= 9.1." end add_pg_decoders @type_map = Type::HashLookupTypeMap.new initialize_type_map(type_map) - @local_tz = execute('SHOW TIME ZONE', 'SCHEMA').first["TimeZone"] + @local_tz = execute("SHOW TIME ZONE", "SCHEMA").first["TimeZone"] @use_insert_returning = @config.key?(:insert_returning) ? self.class.type_cast_config_to_boolean(@config[:insert_returning]) : true end @@ -277,7 +242,7 @@ module ActiveRecord # Is this connection alive and ready for queries? def active? - @connection.query 'SELECT 1' + @connection.query "SELECT 1" true rescue PGError false @@ -294,9 +259,9 @@ module ActiveRecord clear_cache! reset_transaction unless @connection.transaction_status == ::PG::PQTRANS_IDLE - @connection.query 'ROLLBACK' + @connection.query "ROLLBACK" end - @connection.query 'DISCARD ALL' + @connection.query "DISCARD ALL" configure_connection end @@ -321,25 +286,24 @@ module ActiveRecord true end - # Enable standard-conforming strings if available. def set_standard_conforming_strings - old, self.client_min_messages = client_min_messages, 'panic' - execute('SET standard_conforming_strings = on', 'SCHEMA') rescue nil - ensure - self.client_min_messages = old + execute("SET standard_conforming_strings = on", "SCHEMA") end def supports_ddl_transactions? true end + def supports_advisory_locks? + true + end + def supports_explain? true end - # Returns true if pg > 9.1 def supports_extensions? - postgresql_version >= 90100 + true end # Range datatypes weren't introduced until PostgreSQL 9.2 @@ -351,6 +315,24 @@ module ActiveRecord postgresql_version >= 90300 end + def supports_pgcrypto_uuid? + postgresql_version >= 90400 + end + + def get_advisory_lock(lock_id) # :nodoc: + unless lock_id.is_a?(Integer) && lock_id.bit_length <= 63 + raise(ArgumentError, "Postgres requires advisory lock ids to be a signed 64 bit integer") + end + select_value("SELECT pg_try_advisory_lock(#{lock_id});") + end + + def release_advisory_lock(lock_id) # :nodoc: + unless lock_id.is_a?(Integer) && lock_id.bit_length <= 63 + raise(ArgumentError, "Postgres requires advisory lock ids to be a signed 64 bit integer") + end + select_value("SELECT pg_advisory_unlock(#{lock_id})") + end + def enable_extension(name) exec_query("CREATE EXTENSION IF NOT EXISTS \"#{name}\"").tap { reload_type_map @@ -366,7 +348,7 @@ module ActiveRecord def extension_enabled?(name) if supports_extensions? res = exec_query "SELECT EXISTS(SELECT * FROM pg_available_extensions WHERE name = '#{name}' AND installed_version IS NOT NULL) as enabled", - 'SCHEMA' + "SCHEMA" res.cast_values.first end end @@ -381,7 +363,7 @@ module ActiveRecord # Returns the configured supported identifier length supported by PostgreSQL def table_alias_length - @table_alias_length ||= query('SHOW max_identifier_length', 'SCHEMA')[0][0].to_i + @table_alias_length ||= query("SHOW max_identifier_length", "SCHEMA")[0][0].to_i end # Set the authorized user for this session @@ -403,7 +385,7 @@ module ActiveRecord end def lookup_cast_type(sql_type) # :nodoc: - oid = execute("SELECT #{quote(sql_type)}::regtype::oid", "SCHEMA").first['oid'].to_i + oid = execute("SELECT #{quote(sql_type)}::regtype::oid", "SCHEMA").first["oid"].to_i super(oid) end @@ -417,91 +399,104 @@ module ActiveRecord "average" => "avg", } - protected + # Returns the version of the connected PostgreSQL server. + def postgresql_version + @connection.server_version + end - # Returns the version of the connected PostgreSQL server. - def postgresql_version - @connection.server_version - end + private # See http://www.postgresql.org/docs/current/static/errcodes-appendix.html + VALUE_LIMIT_VIOLATION = "22001" + NUMERIC_VALUE_OUT_OF_RANGE = "22003" + NOT_NULL_VIOLATION = "23502" FOREIGN_KEY_VIOLATION = "23503" UNIQUE_VIOLATION = "23505" + SERIALIZATION_FAILURE = "40001" + DEADLOCK_DETECTED = "40P01" def translate_exception(exception, message) return exception unless exception.respond_to?(:result) case exception.result.try(:error_field, PGresult::PG_DIAG_SQLSTATE) when UNIQUE_VIOLATION - RecordNotUnique.new(message, exception) + RecordNotUnique.new(message) when FOREIGN_KEY_VIOLATION - InvalidForeignKey.new(message, exception) + InvalidForeignKey.new(message) + when VALUE_LIMIT_VIOLATION + ValueTooLong.new(message) + when NUMERIC_VALUE_OUT_OF_RANGE + RangeError.new(message) + when NOT_NULL_VIOLATION + NotNullViolation.new(message) + when SERIALIZATION_FAILURE + SerializationFailure.new(message) + when DEADLOCK_DETECTED + Deadlocked.new(message) else super end end - private - - def get_oid_type(oid, fmod, column_name, sql_type = '') # :nodoc: + def get_oid_type(oid, fmod, column_name, sql_type = "") if !type_map.key?(oid) load_additional_types(type_map, [oid]) end type_map.fetch(oid, fmod, sql_type) { warn "unknown OID #{oid}: failed to recognize type of '#{column_name}'. It will be treated as String." - Type::Value.new.tap do |cast_type| + Type.default_value.tap do |cast_type| type_map.register_type(oid, cast_type) end } end - def initialize_type_map(m) # :nodoc: - register_class_with_limit m, 'int2', Type::Integer - register_class_with_limit m, 'int4', Type::Integer - register_class_with_limit m, 'int8', Type::Integer - m.alias_type 'oid', 'int2' - m.register_type 'float4', Type::Float.new - m.alias_type 'float8', 'float4' - m.register_type 'text', Type::Text.new - register_class_with_limit m, 'varchar', Type::String - m.alias_type 'char', 'varchar' - m.alias_type 'name', 'varchar' - m.alias_type 'bpchar', 'varchar' - m.register_type 'bool', Type::Boolean.new - register_class_with_limit m, 'bit', OID::Bit - register_class_with_limit m, 'varbit', OID::BitVarying - m.alias_type 'timestamptz', 'timestamp' - m.register_type 'date', Type::Date.new - - m.register_type 'money', OID::Money.new - m.register_type 'bytea', OID::Bytea.new - m.register_type 'point', OID::Point.new - m.register_type 'hstore', OID::Hstore.new - m.register_type 'json', OID::Json.new - m.register_type 'jsonb', OID::Jsonb.new - m.register_type 'cidr', OID::Cidr.new - m.register_type 'inet', OID::Inet.new - m.register_type 'uuid', OID::Uuid.new - m.register_type 'xml', OID::Xml.new - m.register_type 'tsvector', OID::SpecializedString.new(:tsvector) - m.register_type 'macaddr', OID::SpecializedString.new(:macaddr) - m.register_type 'citext', OID::SpecializedString.new(:citext) - m.register_type 'ltree', OID::SpecializedString.new(:ltree) + def initialize_type_map(m) + register_class_with_limit m, "int2", Type::Integer + register_class_with_limit m, "int4", Type::Integer + register_class_with_limit m, "int8", Type::Integer + m.alias_type "oid", "int2" + m.register_type "float4", Type::Float.new + m.alias_type "float8", "float4" + m.register_type "text", Type::Text.new + register_class_with_limit m, "varchar", Type::String + m.alias_type "char", "varchar" + m.alias_type "name", "varchar" + m.alias_type "bpchar", "varchar" + m.register_type "bool", Type::Boolean.new + register_class_with_limit m, "bit", OID::Bit + register_class_with_limit m, "varbit", OID::BitVarying + m.alias_type "timestamptz", "timestamp" + m.register_type "date", Type::Date.new + + m.register_type "money", OID::Money.new + m.register_type "bytea", OID::Bytea.new + m.register_type "point", OID::Point.new + m.register_type "hstore", OID::Hstore.new + m.register_type "json", OID::Json.new + m.register_type "jsonb", OID::Jsonb.new + m.register_type "cidr", OID::Cidr.new + m.register_type "inet", OID::Inet.new + m.register_type "uuid", OID::Uuid.new + m.register_type "xml", OID::Xml.new + m.register_type "tsvector", OID::SpecializedString.new(:tsvector) + m.register_type "macaddr", OID::SpecializedString.new(:macaddr) + m.register_type "citext", OID::SpecializedString.new(:citext) + m.register_type "ltree", OID::SpecializedString.new(:ltree) + m.register_type "line", OID::SpecializedString.new(:line) + m.register_type "lseg", OID::SpecializedString.new(:lseg) + m.register_type "box", OID::SpecializedString.new(:box) + m.register_type "path", OID::SpecializedString.new(:path) + m.register_type "polygon", OID::SpecializedString.new(:polygon) + m.register_type "circle", OID::SpecializedString.new(:circle) # FIXME: why are we keeping these types as strings? - m.alias_type 'interval', 'varchar' - m.alias_type 'path', 'varchar' - m.alias_type 'line', 'varchar' - m.alias_type 'polygon', 'varchar' - m.alias_type 'circle', 'varchar' - m.alias_type 'lseg', 'varchar' - m.alias_type 'box', 'varchar' - - register_class_with_precision m, 'time', Type::Time - register_class_with_precision m, 'timestamp', OID::DateTime - - m.register_type 'numeric' do |_, fmod, sql_type| + m.alias_type "interval", "varchar" + + register_class_with_precision m, "time", Type::Time + register_class_with_precision m, "timestamp", OID::DateTime + + m.register_type "numeric" do |_, fmod, sql_type| precision = extract_precision(sql_type) scale = extract_scale(sql_type) @@ -524,7 +519,7 @@ module ActiveRecord load_additional_types(m) end - def extract_limit(sql_type) # :nodoc: + def extract_limit(sql_type) case sql_type when /^bigint/i, /^int8/i 8 @@ -536,36 +531,41 @@ module ActiveRecord end # Extracts the value from a PostgreSQL column default definition. - def extract_value_from_default(default) # :nodoc: + def extract_value_from_default(default) case default # Quoted types - when /\A[\(B]?'(.*)'::/m + when /\A[\(B]?'(.*)'.*::"?([\w. ]+)"?(?:\[\])?\z/m + # The default 'now'::date is CURRENT_DATE + if $1 == "now".freeze && $2 == "date".freeze + nil + else $1.gsub("''".freeze, "'".freeze) + end # Boolean types - when 'true'.freeze, 'false'.freeze - default + when "true".freeze, "false".freeze + default # Numeric types - when /\A\(?(-?\d+(\.\d*)?)\)?(::bigint)?\z/ - $1 + when /\A\(?(-?\d+(\.\d*)?)\)?(::bigint)?\z/ + $1 # Object identifier types - when /\A-?\d+\z/ - $1 - else - # Anything else is blank, some user type, or some function - # and we can't know the value of that, so return nil. - nil + when /\A-?\d+\z/ + $1 + else + # Anything else is blank, some user type, or some function + # and we can't know the value of that, so return nil. + nil end end - def extract_default_function(default_value, default) # :nodoc: + def extract_default_function(default_value, default) default if has_default_function?(default_value, default) end - def has_default_function?(default_value, default) # :nodoc: - !default_value && (%r{\w+\(.*\)} === default) + def has_default_function?(default_value, default) + !default_value && (%r{\w+\(.*\)|\(.*\)::\w+} === default) end - def load_additional_types(type_map, oids = nil) # :nodoc: + def load_additional_types(type_map, oids = nil) initializer = OID::TypeMapInitializer.new(type_map) if supports_ranges? @@ -587,52 +587,80 @@ module ActiveRecord query += initializer.query_conditions_for_initial_load(type_map) end - execute_and_clear(query, 'SCHEMA', []) do |records| + execute_and_clear(query, "SCHEMA", []) do |records| initializer.run(records) end end FEATURE_NOT_SUPPORTED = "0A000" #:nodoc: - def execute_and_clear(sql, name, binds) - result = without_prepared_statement?(binds) ? exec_no_cache(sql, name, binds) : - exec_cache(sql, name, binds) + def execute_and_clear(sql, name, binds, prepare: false) + if without_prepared_statement?(binds) + result = exec_no_cache(sql, name, []) + elsif !prepare + result = exec_no_cache(sql, name, binds) + else + result = exec_cache(sql, name, binds) + end ret = yield result result.clear ret end def exec_no_cache(sql, name, binds) - log(sql, name, binds) { @connection.async_exec(sql, []) } + type_casted_binds = type_casted_binds(binds) + log(sql, name, binds, type_casted_binds) do + ActiveSupport::Dependencies.interlock.permit_concurrent_loads do + @connection.async_exec(sql, type_casted_binds) + end + end end def exec_cache(sql, name, binds) stmt_key = prepare_statement(sql) - type_casted_binds = binds.map { |attr| type_cast(attr.value_for_database) } + type_casted_binds = type_casted_binds(binds) - log(sql, name, binds, stmt_key) do - @connection.exec_prepared(stmt_key, type_casted_binds) + log(sql, name, binds, type_casted_binds, stmt_key) do + ActiveSupport::Dependencies.interlock.permit_concurrent_loads do + @connection.exec_prepared(stmt_key, type_casted_binds) + end end rescue ActiveRecord::StatementInvalid => e - pgerror = e.original_exception - - # Get the PG code for the failure. Annoyingly, the code for - # prepared statements whose return value may have changed is - # FEATURE_NOT_SUPPORTED. Check here for more details: - # http://git.postgresql.org/gitweb/?p=postgresql.git;a=blob;f=src/backend/utils/cache/plancache.c#l573 - begin - code = pgerror.result.result_error_field(PGresult::PG_DIAG_SQLSTATE) - rescue - raise e - end - if FEATURE_NOT_SUPPORTED == code + raise unless is_cached_plan_failure?(e) + + # Nothing we can do if we are in a transaction because all commands + # will raise InFailedSQLTransaction + if in_transaction? + raise ActiveRecord::PreparedStatementCacheExpired.new(e.cause.message) + else + # outside of transactions we can simply flush this query and retry @statements.delete sql_key(sql) retry - else - raise e end end + # Annoyingly, the code for prepared statements whose return value may + # have changed is FEATURE_NOT_SUPPORTED. + # + # This covers various different error types so we need to do additional + # work to classify the exception definitively as a + # ActiveRecord::PreparedStatementCacheExpired + # + # Check here for more details: + # http://git.postgresql.org/gitweb/?p=postgresql.git;a=blob;f=src/backend/utils/cache/plancache.c#l573 + CACHED_PLAN_HEURISTIC = "cached plan must not change result type".freeze + def is_cached_plan_failure?(e) + pgerror = e.cause + code = pgerror.result.result_error_field(PGresult::PG_DIAG_SQLSTATE) + code == FEATURE_NOT_SUPPORTED && pgerror.message.include?(CACHED_PLAN_HEURISTIC) + rescue + false + end + + def in_transaction? + open_transactions > 0 + end + # Returns the statement identifier for the client side cache # of statements def sql_key(sql) @@ -661,16 +689,10 @@ module ActiveRecord # connected server's characteristics. def connect @connection = PGconn.connect(@connection_parameters) - - # Money type has a fixed precision of 10 in PostgreSQL 8.2 and below, and as of - # PostgreSQL 8.3 it has a fixed precision of 19. PostgreSQLColumn.extract_precision - # should know about this but can't detect it there, so deal with it here. - OID::Money.precision = (postgresql_version >= 80300) ? 19 : 10 - configure_connection rescue ::PG::Error => error if error.message.include?("does not exist") - raise ActiveRecord::NoDatabaseError.new(error.message, error) + raise ActiveRecord::NoDatabaseError else raise end @@ -682,51 +704,43 @@ module ActiveRecord if @config[:encoding] @connection.set_client_encoding(@config[:encoding]) end - self.client_min_messages = @config[:min_messages] || 'warning' + self.client_min_messages = @config[:min_messages] || "warning" self.schema_search_path = @config[:schema_search_path] || @config[:schema_order] - # Use standard-conforming strings if available so we don't have to do the E'...' dance. + # Use standard-conforming strings so we don't have to do the E'...' dance. set_standard_conforming_strings # If using Active Record's time zone support configure the connection to return # TIMESTAMP WITH ZONE types in UTC. # (SET TIME ZONE does not use an equals sign like other SET variables) if ActiveRecord::Base.default_timezone == :utc - execute("SET time zone 'UTC'", 'SCHEMA') + execute("SET time zone 'UTC'", "SCHEMA") elsif @local_tz - execute("SET time zone '#{@local_tz}'", 'SCHEMA') + execute("SET time zone '#{@local_tz}'", "SCHEMA") end # SET statements from :variables config hash # http://www.postgresql.org/docs/current/static/sql-set.html variables = @config[:variables] || {} variables.map do |k, v| - if v == ':default' || v == :default + if v == ":default" || v == :default # Sets the value to the global or compile default - execute("SET SESSION #{k} TO DEFAULT", 'SCHEMA') + execute("SET SESSION #{k} TO DEFAULT", "SCHEMA") elsif !v.nil? - execute("SET SESSION #{k} TO #{quote(v)}", 'SCHEMA') + execute("SET SESSION #{k} TO #{quote(v)}", "SCHEMA") end end end # Returns the current ID of a table's sequence. - def last_insert_id(sequence_name) #:nodoc: - Integer(last_insert_id_value(sequence_name)) - end - - def last_insert_id_value(sequence_name) - last_insert_id_result(sequence_name).rows.first.first - end - - def last_insert_id_result(sequence_name) #:nodoc: - exec_query("SELECT currval('#{sequence_name}')", 'SQL') + def last_insert_id_result(sequence_name) + exec_query("SELECT currval('#{sequence_name}')", "SQL") end # Returns the list of a table's column names, data types, and default values. # # The underlying query is roughly: - # SELECT column.name, column.type, default.value + # SELECT column.name, column.type, default.value, column.comment # FROM column LEFT JOIN default # ON column.table_id = default.table_id # AND column.num = default.column_num @@ -741,27 +755,28 @@ module ActiveRecord # Query implementation notes: # - format_type includes the column size constraint, e.g. varchar(50) # - ::regclass is a function that gives the id for a table name - def column_definitions(table_name) # :nodoc: - query(<<-end_sql, 'SCHEMA') + def column_definitions(table_name) + query(<<-end_sql, "SCHEMA") SELECT a.attname, format_type(a.atttypid, a.atttypmod), pg_get_expr(d.adbin, d.adrelid), a.attnotnull, a.atttypid, a.atttypmod, (SELECT c.collname FROM pg_collation c, pg_type t - WHERE c.oid = a.attcollation AND t.oid = a.atttypid AND a.attcollation <> t.typcollation) + WHERE c.oid = a.attcollation AND t.oid = a.atttypid AND a.attcollation <> t.typcollation), + col_description(a.attrelid, a.attnum) AS comment 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 + WHERE a.attrelid = #{quote(quote_table_name(table_name))}::regclass AND a.attnum > 0 AND NOT a.attisdropped ORDER BY a.attnum end_sql end - def extract_table_ref_from_insert_sql(sql) # :nodoc: - sql[/into\s+([^\(]*).*values\s*\(/im] + def extract_table_ref_from_insert_sql(sql) + sql[/into\s("[A-Za-z0-9_."\[\]\s]+"|[A-Za-z0-9_."\[\]]+)\s*/im] $1.strip if $1 end - def create_table_definition(name, temporary = false, options = nil, as = nil) # :nodoc: - PostgreSQL::TableDefinition.new native_database_types, name, temporary, options, as + def create_table_definition(*args) + PostgreSQL::TableDefinition.new(*args) end def can_perform_case_insensitive_comparison_for?(column) @@ -770,10 +785,14 @@ module ActiveRecord sql = <<-end_sql SELECT exists( SELECT * FROM pg_proc + WHERE proname = 'lower' + AND proargtypes = ARRAY[#{quote column.sql_type}::regtype]::oidvector + ) OR exists( + SELECT * FROM pg_proc INNER JOIN pg_cast - ON casttarget::text::oidvector = proargtypes + ON ARRAY[casttarget]::oidvector = proargtypes WHERE proname = 'lower' - AND castsource = '#{column.sql_type}'::regtype::oid + AND castsource = #{quote column.sql_type}::regtype ) end_sql execute_and_clear(sql, "SCHEMA", []) do |result| @@ -787,19 +806,18 @@ module ActiveRecord map[Integer] = PG::TextEncoder::Integer.new map[TrueClass] = PG::TextEncoder::Boolean.new map[FalseClass] = PG::TextEncoder::Boolean.new - map[Float] = PG::TextEncoder::Float.new @connection.type_map_for_queries = map end def add_pg_decoders coders_by_name = { - 'int2' => PG::TextDecoder::Integer, - 'int4' => PG::TextDecoder::Integer, - 'int8' => PG::TextDecoder::Integer, - 'oid' => PG::TextDecoder::Integer, - 'float4' => PG::TextDecoder::Float, - 'float8' => PG::TextDecoder::Float, - 'bool' => PG::TextDecoder::Boolean, + "int2" => PG::TextDecoder::Integer, + "int4" => PG::TextDecoder::Integer, + "int8" => PG::TextDecoder::Integer, + "oid" => PG::TextDecoder::Integer, + "float4" => PG::TextDecoder::Float, + "float8" => PG::TextDecoder::Float, + "bool" => PG::TextDecoder::Boolean, } known_coder_types = coders_by_name.keys.map { |n| quote(n) } query = <<-SQL % known_coder_types.join(", ") @@ -809,7 +827,7 @@ module ActiveRecord SQL coders = execute_and_clear(query, "SCHEMA", []) do |result| result - .map { |row| construct_coder(row, coders_by_name[row['typname']]) } + .map { |row| construct_coder(row, coders_by_name[row["typname"]]) } .compact end @@ -820,7 +838,7 @@ module ActiveRecord def construct_coder(row, coder_class) return unless coder_class - coder_class.new(oid: row['oid'].to_i, name: row['typname']) + coder_class.new(oid: row["oid"].to_i, name: row["typname"]) end ActiveRecord::Type.add_modifier({ array: true }, OID::Array, adapter: :postgresql) @@ -829,7 +847,7 @@ module ActiveRecord ActiveRecord::Type.register(:bit_varying, OID::BitVarying, adapter: :postgresql) ActiveRecord::Type.register(:binary, OID::Bytea, adapter: :postgresql) ActiveRecord::Type.register(:cidr, OID::Cidr, adapter: :postgresql) - ActiveRecord::Type.register(:date_time, OID::DateTime, adapter: :postgresql) + ActiveRecord::Type.register(:datetime, OID::DateTime, adapter: :postgresql) ActiveRecord::Type.register(:decimal, OID::Decimal, adapter: :postgresql) ActiveRecord::Type.register(:enum, OID::Enum, adapter: :postgresql) ActiveRecord::Type.register(:hstore, OID::Hstore, adapter: :postgresql) @@ -838,8 +856,7 @@ module ActiveRecord ActiveRecord::Type.register(:jsonb, OID::Jsonb, adapter: :postgresql) ActiveRecord::Type.register(:money, OID::Money, adapter: :postgresql) ActiveRecord::Type.register(:point, OID::Point, adapter: :postgresql) - ActiveRecord::Type.register(:legacy_point, OID::Point, adapter: :postgresql) - ActiveRecord::Type.register(:rails_5_1_point, OID::Rails51Point, adapter: :postgresql) + ActiveRecord::Type.register(:legacy_point, OID::LegacyPoint, adapter: :postgresql) ActiveRecord::Type.register(:uuid, OID::Uuid, adapter: :postgresql) ActiveRecord::Type.register(:vector, OID::Vector, adapter: :postgresql) ActiveRecord::Type.register(:xml, OID::Xml, adapter: :postgresql) diff --git a/activerecord/lib/active_record/connection_adapters/schema_cache.rb b/activerecord/lib/active_record/connection_adapters/schema_cache.rb index 981d5d7a3c..4d339b0a8c 100644 --- a/activerecord/lib/active_record/connection_adapters/schema_cache.rb +++ b/activerecord/lib/active_record/connection_adapters/schema_cache.rb @@ -10,7 +10,7 @@ module ActiveRecord @columns = {} @columns_hash = {} @primary_keys = {} - @tables = {} + @data_sources = {} end def initialize_dup(other) @@ -18,32 +18,48 @@ module ActiveRecord @columns = @columns.dup @columns_hash = @columns_hash.dup @primary_keys = @primary_keys.dup - @tables = @tables.dup + @data_sources = @data_sources.dup + end + + def encode_with(coder) + coder["columns"] = @columns + coder["columns_hash"] = @columns_hash + coder["primary_keys"] = @primary_keys + coder["data_sources"] = @data_sources + coder["version"] = ActiveRecord::Migrator.current_version + end + + def init_with(coder) + @columns = coder["columns"] + @columns_hash = coder["columns_hash"] + @primary_keys = coder["primary_keys"] + @data_sources = coder["data_sources"] + @version = coder["version"] end def primary_keys(table_name) - @primary_keys[table_name] ||= table_exists?(table_name) ? connection.primary_key(table_name) : nil + @primary_keys[table_name] ||= data_source_exists?(table_name) ? connection.primary_key(table_name) : nil end # A cached lookup for table existence. - def table_exists?(name) - prepare_tables if @tables.empty? - return @tables[name] if @tables.key? name + def data_source_exists?(name) + prepare_data_sources if @data_sources.empty? + return @data_sources[name] if @data_sources.key? name - @tables[name] = connection.table_exists?(name) + @data_sources[name] = connection.data_source_exists?(name) end # Add internal cache for table with +table_name+. def add(table_name) - if table_exists?(table_name) + if data_source_exists?(table_name) primary_keys(table_name) columns(table_name) columns_hash(table_name) end end - def tables(name) - @tables[name] + def data_sources(name) + @data_sources[name] end # Get the columns for a table @@ -64,36 +80,36 @@ module ActiveRecord @columns.clear @columns_hash.clear @primary_keys.clear - @tables.clear + @data_sources.clear @version = nil end def size - [@columns, @columns_hash, @primary_keys, @tables].map(&:size).inject :+ + [@columns, @columns_hash, @primary_keys, @data_sources].map(&:size).inject :+ end - # Clear out internal caches for table with +table_name+. - def clear_table_cache!(table_name) - @columns.delete table_name - @columns_hash.delete table_name - @primary_keys.delete table_name - @tables.delete table_name + # Clear out internal caches for the data source +name+. + def clear_data_source_cache!(name) + @columns.delete name + @columns_hash.delete name + @primary_keys.delete name + @data_sources.delete name end def marshal_dump # if we get current version during initialization, it happens stack over flow. @version = ActiveRecord::Migrator.current_version - [@version, @columns, @columns_hash, @primary_keys, @tables] + [@version, @columns, @columns_hash, @primary_keys, @data_sources] end def marshal_load(array) - @version, @columns, @columns_hash, @primary_keys, @tables = array + @version, @columns, @columns_hash, @primary_keys, @data_sources = array end private - def prepare_tables - connection.tables.each { |table| @tables[table] = true } + def prepare_data_sources + connection.data_sources.each { |source| @data_sources[source] = true } end end end diff --git a/activerecord/lib/active_record/connection_adapters/sql_type_metadata.rb b/activerecord/lib/active_record/connection_adapters/sql_type_metadata.rb index ccb7e154ee..9e12ae0de8 100644 --- a/activerecord/lib/active_record/connection_adapters/sql_type_metadata.rb +++ b/activerecord/lib/active_record/connection_adapters/sql_type_metadata.rb @@ -24,9 +24,9 @@ module ActiveRecord protected - def attributes_for_hash - [self.class, sql_type, type, limit, precision, scale] - end + def attributes_for_hash + [self.class, sql_type, type, limit, precision, scale] + end end end end diff --git a/activerecord/lib/active_record/connection_adapters/sqlite3/explain_pretty_printer.rb b/activerecord/lib/active_record/connection_adapters/sqlite3/explain_pretty_printer.rb new file mode 100644 index 0000000000..6fe3e1211e --- /dev/null +++ b/activerecord/lib/active_record/connection_adapters/sqlite3/explain_pretty_printer.rb @@ -0,0 +1,19 @@ +module ActiveRecord + module ConnectionAdapters + module SQLite3 + class ExplainPrettyPrinter # :nodoc: + # Pretty prints the result of an EXPLAIN QUERY PLAN in a way that resembles + # the output of the SQLite shell: + # + # 0|0|0|SEARCH TABLE users USING INTEGER PRIMARY KEY (rowid=?) (~1 rows) + # 0|1|1|SCAN TABLE posts (~100000 rows) + # + def pp(result) + result.rows.map do |row| + row.join("|") + end.join("\n") + "\n" + end + end + end + end +end diff --git a/activerecord/lib/active_record/connection_adapters/sqlite3/quoting.rb b/activerecord/lib/active_record/connection_adapters/sqlite3/quoting.rb new file mode 100644 index 0000000000..f01ed67b0f --- /dev/null +++ b/activerecord/lib/active_record/connection_adapters/sqlite3/quoting.rb @@ -0,0 +1,48 @@ +module ActiveRecord + module ConnectionAdapters + module SQLite3 + module Quoting # :nodoc: + def quote_string(s) + @connection.class.quote(s) + end + + def quote_table_name_for_assignment(table, attr) + quote_column_name(attr) + end + + def quote_column_name(name) + @quoted_column_names[name] ||= %Q("#{super.gsub('"', '""')}").freeze + end + + def quoted_time(value) + quoted_date(value) + end + + private + + def _quote(value) + if value.is_a?(Type::Binary::Data) + "x'#{value.hex}'" + else + super + end + end + + def _type_cast(value) + case value + when BigDecimal + value.to_f + when String + if value.encoding == Encoding::ASCII_8BIT + super(value.encode(Encoding::UTF_8)) + else + super + end + else + super + end + end + end + end + end +end diff --git a/activerecord/lib/active_record/connection_adapters/sqlite3/schema_creation.rb b/activerecord/lib/active_record/connection_adapters/sqlite3/schema_creation.rb index fe1dcbd710..70c0d28830 100644 --- a/activerecord/lib/active_record/connection_adapters/sqlite3/schema_creation.rb +++ b/activerecord/lib/active_record/connection_adapters/sqlite3/schema_creation.rb @@ -3,6 +3,13 @@ module ActiveRecord module SQLite3 class SchemaCreation < AbstractAdapter::SchemaCreation private + + def column_options(o) + options = super + options[:null] = false if o.primary_key + options + end + def add_column_options!(sql, options) if options[:collation] sql << " COLLATE \"#{options[:collation]}\"" diff --git a/activerecord/lib/active_record/connection_adapters/sqlite3/schema_definitions.rb b/activerecord/lib/active_record/connection_adapters/sqlite3/schema_definitions.rb new file mode 100644 index 0000000000..d0b38dff4c --- /dev/null +++ b/activerecord/lib/active_record/connection_adapters/sqlite3/schema_definitions.rb @@ -0,0 +1,23 @@ +module ActiveRecord + module ConnectionAdapters + module SQLite3 + module ColumnMethods + def primary_key(name, type = :primary_key, **options) + if options.delete(:auto_increment) == true && %i(integer bigint).include?(type) + type = :primary_key + end + + super + end + end + + class TableDefinition < ActiveRecord::ConnectionAdapters::TableDefinition + include ColumnMethods + end + + class Table < ActiveRecord::ConnectionAdapters::Table + include ColumnMethods + end + end + end +end diff --git a/activerecord/lib/active_record/connection_adapters/sqlite3/schema_dumper.rb b/activerecord/lib/active_record/connection_adapters/sqlite3/schema_dumper.rb new file mode 100644 index 0000000000..c027fef83c --- /dev/null +++ b/activerecord/lib/active_record/connection_adapters/sqlite3/schema_dumper.rb @@ -0,0 +1,13 @@ +module ActiveRecord + module ConnectionAdapters + module SQLite3 + module ColumnDumper + private + + def default_primary_key?(column) + schema_type(column) == :integer + end + end + end + end +end diff --git a/activerecord/lib/active_record/connection_adapters/sqlite3_adapter.rb b/activerecord/lib/active_record/connection_adapters/sqlite3_adapter.rb index 7c809b088c..297e2997a9 100644 --- a/activerecord/lib/active_record/connection_adapters/sqlite3_adapter.rb +++ b/activerecord/lib/active_record/connection_adapters/sqlite3_adapter.rb @@ -1,13 +1,16 @@ -require 'active_record/connection_adapters/abstract_adapter' -require 'active_record/connection_adapters/statement_pool' -require 'active_record/connection_adapters/sqlite3/schema_creation' +require "active_record/connection_adapters/abstract_adapter" +require "active_record/connection_adapters/statement_pool" +require "active_record/connection_adapters/sqlite3/explain_pretty_printer" +require "active_record/connection_adapters/sqlite3/quoting" +require "active_record/connection_adapters/sqlite3/schema_creation" +require "active_record/connection_adapters/sqlite3/schema_definitions" +require "active_record/connection_adapters/sqlite3/schema_dumper" -gem 'sqlite3', '~> 1.3.6' -require 'sqlite3' +gem "sqlite3", "~> 1.3.6" +require "sqlite3" module ActiveRecord module ConnectionHandling # :nodoc: - # sqlite3 adapter reuses sqlite_connection. def sqlite3_connection(config) # Require database. unless config[:database] @@ -17,7 +20,7 @@ module ActiveRecord # Allow database path relative to Rails.root, but only if the database # path is not the special path that tells sqlite to build a database only # in memory. - if ':memory:' != config[:database] + if ":memory:" != config[:database] config[:database] = File.expand_path(config[:database], Rails.root) if defined?(Rails.root) dirname = File.dirname(config[:database]) Dir.mkdir(dirname) unless File.directory?(dirname) @@ -25,7 +28,7 @@ module ActiveRecord db = SQLite3::Database.new( config[:database].to_s, - :results_as_hash => true + results_as_hash: true ) db.busy_timeout(ConnectionAdapters::SQLite3Adapter.type_cast_config_to_integer(config[:timeout])) if config[:timeout] @@ -33,7 +36,7 @@ module ActiveRecord ConnectionAdapters::SQLite3Adapter.new(db, logger, nil, config) rescue Errno::ENOENT => error if error.message.include?("No such file or directory") - raise ActiveRecord::NoDatabaseError.new(error.message, error) + raise ActiveRecord::NoDatabaseError else raise end @@ -48,11 +51,13 @@ module ActiveRecord # # * <tt>:database</tt> - Path to the database file. class SQLite3Adapter < AbstractAdapter - ADAPTER_NAME = 'SQLite'.freeze - include Savepoints + ADAPTER_NAME = "SQLite".freeze + + include SQLite3::Quoting + include SQLite3::ColumnDumper NATIVE_DATABASE_TYPES = { - primary_key: 'INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL', + primary_key: "INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL", string: { name: "varchar" }, text: { name: "text" }, integer: { name: "integer" }, @@ -65,46 +70,31 @@ module ActiveRecord boolean: { name: "boolean" } } - class Version - include Comparable - - def initialize(version_string) - @version = version_string.split('.').map(&:to_i) - end - - def <=>(version_string) - @version <=> version_string.split('.').map(&:to_i) - end - end - class StatementPool < ConnectionAdapters::StatementPool private - def dealloc(stmt) - stmt[:stmt].close unless stmt[:stmt].closed? - end + def dealloc(stmt) + stmt[:stmt].close unless stmt[:stmt].closed? + end + end + + def update_table_definition(table_name, base) # :nodoc: + SQLite3::Table.new(table_name, base) end def schema_creation # :nodoc: SQLite3::SchemaCreation.new self end + def arel_visitor # :nodoc: + Arel::Visitors::SQLite.new(self) + end + def initialize(connection, logger, connection_options, config) - super(connection, logger) + super(connection, logger, config) @active = nil - @statements = StatementPool.new(@connection, - self.class.type_cast_config_to_integer(config.fetch(:statement_limit) { 1000 })) - @config = config - - @visitor = Arel::Visitors::SQLite.new self - @quoted_column_names = {} - - if self.class.type_cast_config_to_boolean(config.fetch(:prepared_statements) { true }) - @prepared_statements = true - else - @prepared_statements = false - end + @statements = StatementPool.new(self.class.type_cast_config_to_integer(config[:statement_limit])) end def supports_ddl_transactions? @@ -116,7 +106,7 @@ module ActiveRecord end def supports_partial_index? - sqlite_version >= '3.8.0' + sqlite_version >= "3.8.0" end # Returns true, since this connection adapter supports prepared statement @@ -142,6 +132,14 @@ module ActiveRecord true end + def supports_datetime_with_precision? + true + end + + def supports_multi_insert? + sqlite_version >= "3.7.11" + end + def active? @active != false end @@ -163,8 +161,12 @@ module ActiveRecord true end + def valid_type?(type) + true + end + # Returns 62. SQLite supports index names up to 64 - # characters. The rest is used by rails internally to perform + # characters. The rest is used by Rails internally to perform # temporary rename operations def allowed_index_name_length index_name_length - 2 @@ -183,96 +185,49 @@ module ActiveRecord true end - # QUOTING ================================================== - - def _quote(value) # :nodoc: - case value - when Type::Binary::Data - "x'#{value.hex}'" - else - super - end - end - - def _type_cast(value) # :nodoc: - case value - when BigDecimal - value.to_f - when String - if value.encoding == Encoding::ASCII_8BIT - super(value.encode(Encoding::UTF_8)) - else - super - end - else - super - end - end - - def quote_string(s) #:nodoc: - @connection.class.quote(s) - end - - def quote_table_name_for_assignment(table, attr) - quote_column_name(attr) - end - - def quote_column_name(name) #:nodoc: - @quoted_column_names[name] ||= %Q("#{name.to_s.gsub('"', '""')}") - end - #-- # DATABASE STATEMENTS ====================================== #++ def explain(arel, binds = []) sql = "EXPLAIN QUERY PLAN #{to_sql(arel, binds)}" - ExplainPrettyPrinter.new.pp(exec_query(sql, 'EXPLAIN', [])) - end - - class ExplainPrettyPrinter - # Pretty prints the result of a EXPLAIN QUERY PLAN in a way that resembles - # the output of the SQLite shell: - # - # 0|0|0|SEARCH TABLE users USING INTEGER PRIMARY KEY (rowid=?) (~1 rows) - # 0|1|1|SCAN TABLE posts (~100000 rows) - # - def pp(result) # :nodoc: - result.rows.map do |row| - row.join('|') - end.join("\n") + "\n" - end - end - - def exec_query(sql, name = nil, binds = []) - type_casted_binds = binds.map { |attr| type_cast(attr.value_for_database) } - - log(sql, name, binds) do - # Don't cache statements if they are not prepared - if without_prepared_statement?(binds) - stmt = @connection.prepare(sql) - begin - cols = stmt.columns + SQLite3::ExplainPrettyPrinter.new.pp(exec_query(sql, "EXPLAIN", [])) + end + + def exec_query(sql, name = nil, binds = [], prepare: false) + type_casted_binds = type_casted_binds(binds) + + log(sql, name, binds, type_casted_binds) do + ActiveSupport::Dependencies.interlock.permit_concurrent_loads do + # Don't cache statements if they are not prepared + unless prepare + stmt = @connection.prepare(sql) + begin + cols = stmt.columns + unless without_prepared_statement?(binds) + stmt.bind_params(type_casted_binds) + end + records = stmt.to_a + ensure + stmt.close + end + else + cache = @statements[sql] ||= { + stmt: @connection.prepare(sql) + } + stmt = cache[:stmt] + cols = cache[:cols] ||= stmt.columns + stmt.reset! + stmt.bind_params(type_casted_binds) records = stmt.to_a - ensure - stmt.close end - stmt = records - else - cache = @statements[sql] ||= { - :stmt => @connection.prepare(sql) - } - stmt = cache[:stmt] - cols = cache[:cols] ||= stmt.columns - stmt.reset! - stmt.bind_params type_casted_binds - end - ActiveRecord::Result.new(cols, stmt.to_a) + ActiveRecord::Result.new(cols, records) + end end end - def exec_delete(sql, name = 'SQL', binds = []) + def exec_delete(sql, name = "SQL", binds = []) exec_query(sql, name, binds) @connection.changes end @@ -283,82 +238,85 @@ module ActiveRecord end def execute(sql, name = nil) #:nodoc: - log(sql, name) { @connection.execute(sql) } + log(sql, name) do + ActiveSupport::Dependencies.interlock.permit_concurrent_loads do + @connection.execute(sql) + end + end end - def update_sql(sql, name = nil) #:nodoc: - super - @connection.changes + def begin_db_transaction #:nodoc: + log("begin transaction", nil) { @connection.transaction } end - def delete_sql(sql, name = nil) #:nodoc: - sql += " WHERE 1=1" unless sql =~ /WHERE/i - super sql, name + def commit_db_transaction #:nodoc: + log("commit transaction", nil) { @connection.commit } end - def insert_sql(sql, name = nil, pk = nil, id_value = nil, sequence_name = nil) #:nodoc: - super - id_value || @connection.last_insert_row_id + def exec_rollback_db_transaction #:nodoc: + log("rollback transaction", nil) { @connection.rollback } end - alias :create :insert_sql - def select_rows(sql, name = nil, binds = []) - exec_query(sql, name, binds).rows - end + # SCHEMA STATEMENTS ======================================== - def begin_db_transaction #:nodoc: - log('begin transaction',nil) { @connection.transaction } + def tables # :nodoc: + select_values("SELECT name FROM sqlite_master WHERE type = 'table' AND name <> 'sqlite_sequence'", "SCHEMA") end - def commit_db_transaction #:nodoc: - log('commit transaction',nil) { @connection.commit } + def data_sources # :nodoc: + select_values("SELECT name FROM sqlite_master WHERE type IN ('table','view') AND name <> 'sqlite_sequence'", "SCHEMA") end - def exec_rollback_db_transaction #:nodoc: - log('rollback transaction',nil) { @connection.rollback } + def views # :nodoc: + select_values("SELECT name FROM sqlite_master WHERE type = 'view' AND name <> 'sqlite_sequence'", "SCHEMA") end - # SCHEMA STATEMENTS ======================================== + def table_exists?(table_name) # :nodoc: + return false unless table_name.present? - def tables(name = nil, table_name = nil) #:nodoc: - sql = <<-SQL - SELECT name - FROM sqlite_master - WHERE (type = 'table' OR type = 'view') AND NOT name = 'sqlite_sequence' - SQL - sql << " AND name = #{quote_table_name(table_name)}" if table_name + sql = "SELECT name FROM sqlite_master WHERE type = 'table' AND name <> 'sqlite_sequence'" + sql << " AND name = #{quote(table_name)}" - exec_query(sql, 'SCHEMA').map do |row| - row['name'] - end + select_values(sql, "SCHEMA").any? end - def table_exists?(table_name) - table_name && tables(nil, table_name).any? + def data_source_exists?(table_name) # :nodoc: + return false unless table_name.present? + + sql = "SELECT name FROM sqlite_master WHERE type IN ('table','view') AND name <> 'sqlite_sequence'" + sql << " AND name = #{quote(table_name)}" + + select_values(sql, "SCHEMA").any? end - # Returns an array of +Column+ objects for the table specified by +table_name+. - def columns(table_name) #:nodoc: - table_structure(table_name).map do |field| - case field["dflt_value"] - when /^null$/i - field["dflt_value"] = nil - when /^'(.*)'$/m - field["dflt_value"] = $1.gsub("''", "'") - when /^"(.*)"$/m - field["dflt_value"] = $1.gsub('""', '"') - end + def view_exists?(view_name) # :nodoc: + return false unless view_name.present? + + sql = "SELECT name FROM sqlite_master WHERE type = 'view' AND name <> 'sqlite_sequence'" + sql << " AND name = #{quote(view_name)}" - collation = field['collation'] - sql_type = field['type'] - type_metadata = fetch_type_metadata(sql_type) - new_column(field['name'], field['dflt_value'], type_metadata, field['notnull'].to_i == 0, nil, collation) + select_values(sql, "SCHEMA").any? + end + + def new_column_from_field(table_name, field) # :nondoc: + case field["dflt_value"] + when /^null$/i + field["dflt_value"] = nil + when /^'(.*)'$/m + field["dflt_value"] = $1.gsub("''", "'") + when /^"(.*)"$/m + field["dflt_value"] = $1.gsub('""', '"') end + + collation = field["collation"] + sql_type = field["type"] + type_metadata = fetch_type_metadata(sql_type) + new_column(field["name"], field["dflt_value"], type_metadata, field["notnull"].to_i == 0, table_name, nil, collation) end # Returns an array of indexes for the given table. def indexes(table_name, name = nil) #:nodoc: - exec_query("PRAGMA index_list(#{quote_table_name(table_name)})", 'SCHEMA').map do |row| + exec_query("PRAGMA index_list(#{quote_table_name(table_name)})", "SCHEMA").map do |row| sql = <<-SQL SELECT sql FROM sqlite_master @@ -368,26 +326,26 @@ module ActiveRecord FROM sqlite_temp_master WHERE name=#{quote(row['name'])} AND type='index' SQL - index_sql = exec_query(sql).first['sql'] + index_sql = exec_query(sql).first["sql"] match = /\sWHERE\s+(.+)$/i.match(index_sql) where = match[1] if match IndexDefinition.new( table_name, - row['name'], - row['unique'] != 0, + row["name"], + row["unique"] != 0, exec_query("PRAGMA index_info('#{row['name']}')", "SCHEMA").map { |col| - col['name'] + col["name"] }, nil, nil, where) end end - def primary_key(table_name) #:nodoc: - pks = table_structure(table_name).select { |f| f['pk'] > 0 } - return nil unless pks.count == 1 - pks[0]['name'] + def primary_keys(table_name) # :nodoc: + pks = table_structure(table_name).select { |f| f["pk"] > 0 } + pks.sort_by { |f| f["pk"] }.map { |f| f["name"] } end - def remove_index!(table_name, index_name) #:nodoc: + def remove_index(table_name, options = {}) #:nodoc: + index_name = index_name_for_remove(table_name, options) exec_query "DROP INDEX #{quote_column_name(index_name)}" end @@ -448,7 +406,7 @@ module ActiveRecord self.default = options[:default] if include_default self.null = options[:null] if options.include?(:null) self.precision = options[:precision] if options.include?(:precision) - self.scale = options[:scale] if options.include?(:scale) + self.scale = options[:scale] if options.include?(:scale) self.collation = options[:collation] if options.include?(:collation) end end @@ -456,35 +414,36 @@ module ActiveRecord def rename_column(table_name, column_name, new_column_name) #:nodoc: column = column_for(table_name, column_name) - alter_table(table_name, rename: {column.name => new_column_name.to_s}) + alter_table(table_name, rename: { column.name => new_column_name.to_s }) rename_column_indexes(table_name, column.name, new_column_name) end - protected + private def table_structure(table_name) - structure = exec_query("PRAGMA table_info(#{quote_table_name(table_name)})", 'SCHEMA') + structure = exec_query("PRAGMA table_info(#{quote_table_name(table_name)})", "SCHEMA") raise(ActiveRecord::StatementInvalid, "Could not find table '#{table_name}'") if structure.empty? table_structure_with_collation(table_name, structure) end + alias column_definitions table_structure - def alter_table(table_name, options = {}) #:nodoc: + def alter_table(table_name, options = {}) altered_table_name = "a#{table_name}" - caller = lambda {|definition| yield definition if block_given?} + caller = lambda { |definition| yield definition if block_given? } transaction do move_table(table_name, altered_table_name, - options.merge(:temporary => true)) + options.merge(temporary: true)) move_table(altered_table_name, table_name, &caller) end end - def move_table(from, to, options = {}, &block) #:nodoc: + def move_table(from, to, options = {}, &block) copy_table(from, to, options, &block) drop_table(from) end - def copy_table(from, to, options = {}) #:nodoc: + def copy_table(from, to, options = {}) from_primary_key = primary_key(from) options[:id] = false create_table(to, options) do |definition| @@ -498,9 +457,9 @@ module ActiveRecord 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, collation: column.collation) + limit: column.limit, default: column.default, + precision: column.precision, scale: column.scale, + null: column.null, collation: column.collation) end yield @definition if block_given? end @@ -510,7 +469,7 @@ module ActiveRecord options[:rename] || {}) end - def copy_table_indexes(from, to, rename = {}) #:nodoc: + def copy_table_indexes(from, to, rename = {}) indexes(from).each do |index| name = index.name if to == "a#{from}" @@ -520,7 +479,7 @@ module ActiveRecord end to_column_names = columns(to).map(&:name) - columns = index.columns.map {|c| rename[c] || c }.select do |column| + columns = index.columns.map { |c| rename[c] || c }.select do |column| to_column_names.include?(column) end @@ -533,21 +492,21 @@ module ActiveRecord end end - def copy_table_contents(from, to, columns, rename = {}) #:nodoc: - column_mappings = Hash[columns.map {|name| [name, name]}] + def copy_table_contents(from, to, columns, rename = {}) + column_mappings = Hash[columns.map { |name| [name, name] }] rename.each { |a| column_mappings[a.last] = a.first } from_columns = columns(from).collect(&:name) - columns = columns.find_all{|col| from_columns.include?(column_mappings[col])} + columns = columns.find_all { |col| from_columns.include?(column_mappings[col]) } from_columns_to_copy = columns.map { |col| column_mappings[col] } - quoted_columns = columns.map { |col| quote_column_name(col) } * ',' - quoted_from_columns = from_columns_to_copy.map { |col| quote_column_name(col) } * ',' + quoted_columns = columns.map { |col| quote_column_name(col) } * "," + quoted_from_columns = from_columns_to_copy.map { |col| quote_column_name(col) } * "," exec_query("INSERT INTO #{quote_table_name(to)} (#{quoted_columns}) SELECT #{quoted_from_columns} FROM #{quote_table_name(from)}") end def sqlite_version - @sqlite_version ||= SQLite3Adapter::Version.new(select_value('select sqlite_version(*)')) + @sqlite_version ||= SQLite3Adapter::Version.new(select_value("select sqlite_version(*)")) end def translate_exception(exception, message) @@ -557,43 +516,46 @@ module ActiveRecord # Older versions of SQLite return: # column *column_name* is not unique when /column(s)? .* (is|are) not unique/, /UNIQUE constraint failed: .*/ - RecordNotUnique.new(message, exception) + RecordNotUnique.new(message) + when /.* may not be NULL/, /NOT NULL constraint failed: .*/ + NotNullViolation.new(message) else super end end - private COLLATE_REGEX = /.*\"(\w+)\".*collate\s+\"(\w+)\".*/i.freeze def table_structure_with_collation(table_name, basic_structure) collation_hash = {} - sql = "SELECT sql FROM - (SELECT * FROM sqlite_master UNION ALL - SELECT * FROM sqlite_temp_master) - WHERE type='table' and name='#{ table_name }' \;" + sql = <<-SQL + SELECT sql FROM + (SELECT * FROM sqlite_master UNION ALL + SELECT * FROM sqlite_temp_master) + WHERE type = 'table' AND name = #{quote(table_name)} + SQL # Result will have following sample string # CREATE TABLE "users" ("id" INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, # "password_digest" varchar COLLATE "NOCASE"); - result = exec_query(sql, 'SCHEMA').first + result = exec_query(sql, "SCHEMA").first if result - # Splitting with left parantheses and picking up last will return all + # Splitting with left parentheses and picking up last will return all # columns separated with comma(,). - columns_string = result["sql"].split('(').last + columns_string = result["sql"].split("(").last - columns_string.split(',').each do |column_string| + columns_string.split(",").each do |column_string| # This regex will match the column name and collation type and will save # the value in $1 and $2 respectively. - collation_hash[$1] = $2 if (COLLATE_REGEX =~ column_string) + collation_hash[$1] = $2 if COLLATE_REGEX =~ column_string end basic_structure.map! do |column| - column_name = column['name'] + column_name = column["name"] if collation_hash.has_key? column_name - column['collation'] = collation_hash[column_name] + column["collation"] = collation_hash[column_name] end column @@ -602,6 +564,10 @@ module ActiveRecord basic_structure.to_hash end end + + def create_table_definition(*args) + SQLite3::TableDefinition.new(*args) + end end end end diff --git a/activerecord/lib/active_record/connection_adapters/statement_pool.rb b/activerecord/lib/active_record/connection_adapters/statement_pool.rb index 82e9ef3d3d..790db56185 100644 --- a/activerecord/lib/active_record/connection_adapters/statement_pool.rb +++ b/activerecord/lib/active_record/connection_adapters/statement_pool.rb @@ -1,12 +1,13 @@ module ActiveRecord module ConnectionAdapters - class StatementPool + class StatementPool # :nodoc: include Enumerable - def initialize(connection, max = 1000) - @cache = Hash.new { |h,pid| h[pid] = {} } - @connection = connection - @max = max + DEFAULT_STATEMENT_LIMIT = 1000 + + def initialize(statement_limit = nil) + @cache = Hash.new { |h, pid| h[pid] = {} } + @statement_limit = statement_limit || DEFAULT_STATEMENT_LIMIT end def each(&block) @@ -26,7 +27,7 @@ module ActiveRecord end def []=(sql, stmt) - while @max <= cache.size + while @statement_limit <= cache.size dealloc(cache.shift.last) end cache[sql] = stmt @@ -46,13 +47,13 @@ module ActiveRecord private - def cache - @cache[Process.pid] - end + def cache + @cache[Process.pid] + end - def dealloc(stmt) - raise NotImplementedError - end + def dealloc(stmt) + raise NotImplementedError + end end end end diff --git a/activerecord/lib/active_record/connection_handling.rb b/activerecord/lib/active_record/connection_handling.rb index d6b661ff76..2ede92feff 100644 --- a/activerecord/lib/active_record/connection_handling.rb +++ b/activerecord/lib/active_record/connection_handling.rb @@ -8,7 +8,7 @@ module ActiveRecord # example for regular databases (MySQL, PostgreSQL, etc): # # ActiveRecord::Base.establish_connection( - # adapter: "mysql", + # adapter: "mysql2", # host: "localhost", # username: "myuser", # password: "mypass", @@ -35,26 +35,27 @@ module ActiveRecord # "postgres://myuser:mypass@localhost/somedatabase" # ) # - # In case <tt>ActiveRecord::Base.configurations</tt> is set (Rails - # automatically loads the contents of config/database.yml into it), + # In case {ActiveRecord::Base.configurations}[rdoc-ref:Core.configurations] + # is set (Rails automatically loads the contents of config/database.yml into it), # a symbol can also be given as argument, representing a key in the # configuration hash: # # ActiveRecord::Base.establish_connection(:production) # - # The exceptions AdapterNotSpecified, AdapterNotFound and ArgumentError + # The exceptions AdapterNotSpecified, AdapterNotFound and +ArgumentError+ # may be returned on an error. - def establish_connection(spec = nil) - spec ||= DEFAULT_ENV.call.to_sym - resolver = ConnectionAdapters::ConnectionSpecification::Resolver.new configurations - spec = resolver.spec(spec) + def establish_connection(config = nil) + raise "Anonymous class is not allowed." unless name - unless respond_to?(spec.adapter_method) - raise AdapterNotFound, "database configuration specifies nonexistent #{spec.config[:adapter]} adapter" - end + config ||= DEFAULT_ENV.call.to_sym + spec_name = self == Base ? "primary" : name + self.connection_specification_name = spec_name + + resolver = ConnectionAdapters::ConnectionSpecification::Resolver.new(Base.configurations) + spec = resolver.resolve(config).symbolize_keys + spec[:name] = spec_name - remove_connection - connection_handler.establish_connection self, spec + connection_handler.establish_connection(spec) end class MergeAndResolveDefaultUrlConfig # :nodoc: @@ -72,7 +73,7 @@ module ActiveRecord private def config @raw_config.dup.tap do |cfg| - if url = ENV['DATABASE_URL'] + if url = ENV["DATABASE_URL"] cfg[@env] ||= {} cfg[@env]["url"] ||= url end @@ -87,12 +88,14 @@ module ActiveRecord retrieve_connection end - def connection_id - ActiveRecord::RuntimeRegistry.connection_id ||= Thread.current.object_id - end + attr_writer :connection_specification_name - def connection_id=(connection_id) - ActiveRecord::RuntimeRegistry.connection_id = connection_id + # Return the specification name from the current class or its parent. + def connection_specification_name + if !defined?(@connection_specification_name) || @connection_specification_name.nil? + return self == Base ? "primary" : superclass.connection_specification_name + end + @connection_specification_name end # Returns the configuration of the associated connection as a hash: @@ -106,20 +109,28 @@ module ActiveRecord end def connection_pool - connection_handler.retrieve_connection_pool(self) or raise ConnectionNotEstablished + connection_handler.retrieve_connection_pool(connection_specification_name) || raise(ConnectionNotEstablished) end def retrieve_connection - connection_handler.retrieve_connection(self) + connection_handler.retrieve_connection(connection_specification_name) end # Returns +true+ if Active Record is connected. def connected? - connection_handler.connected?(self) + connection_handler.connected?(connection_specification_name) end - def remove_connection(klass = self) - connection_handler.remove_connection(klass) + def remove_connection(name = nil) + name ||= @connection_specification_name if defined?(@connection_specification_name) + # if removing a connection that has a pool, we reset the + # connection_specification_name so it will use the parent + # pool. + if connection_handler.retrieve_connection_pool(name) + self.connection_specification_name = nil + end + + connection_handler.remove_connection(name) end def clear_cache! # :nodoc: @@ -127,6 +138,6 @@ module ActiveRecord end delegate :clear_active_connections!, :clear_reloadable_connections!, - :clear_all_connections!, :to => :connection_handler + :clear_all_connections!, to: :connection_handler end end diff --git a/activerecord/lib/active_record/core.rb b/activerecord/lib/active_record/core.rb index 8a014e682e..d4836faa4b 100644 --- a/activerecord/lib/active_record/core.rb +++ b/activerecord/lib/active_record/core.rb @@ -1,7 +1,7 @@ -require 'thread' -require 'active_support/core_ext/hash/indifferent_access' -require 'active_support/core_ext/object/duplicable' -require 'active_support/core_ext/string/filters' +require "thread" +require "active_support/core_ext/hash/indifferent_access" +require "active_support/core_ext/object/duplicable" +require "active_support/core_ext/string/filters" module ActiveRecord module Core @@ -72,6 +72,34 @@ module ActiveRecord ## # :singleton-method: + # Specifies if an error should be raised if the query has an order being + # ignored when doing batch queries. Useful in applications where the + # scope being ignored is error-worthy, rather than a warning. + mattr_accessor :error_on_ignored_order, instance_writer: false + self.error_on_ignored_order = false + + def self.error_on_ignored_order_or_limit + ActiveSupport::Deprecation.warn(<<-MSG.squish) + The flag error_on_ignored_order_or_limit is deprecated. Limits are + now supported. Please use error_on_ignored_order instead. + MSG + error_on_ignored_order + end + + def error_on_ignored_order_or_limit + self.class.error_on_ignored_order_or_limit + end + + def self.error_on_ignored_order_or_limit=(value) + ActiveSupport::Deprecation.warn(<<-MSG.squish) + The flag error_on_ignored_order_or_limit is deprecated. Limits are + now supported. Please use error_on_ignored_order= instead. + MSG + self.error_on_ignored_order = value + end + + ## + # :singleton-method: # Specify whether or not to use timestamps for migration versions mattr_accessor :timestamped_migrations, instance_writer: false self.timestamped_migrations = true @@ -128,7 +156,7 @@ module ActiveRecord end def initialize_find_by_cache # :nodoc: - @find_by_statement_cache = {}.extend(Mutex_m) + @find_by_statement_cache = { true => {}.extend(Mutex_m), false => {}.extend(Mutex_m) } end def inherited(child_class) # :nodoc: @@ -146,12 +174,12 @@ module ActiveRecord columns_hash.include?(inheritance_column) || ids.first.kind_of?(Array) - id = ids.first + id = ids.first if ActiveRecord::Base === id id = id.id ActiveSupport::Deprecation.warn(<<-MSG.squish) You are passing an instance of ActiveRecord::Base to `find`. - Please pass the id of the object by calling `.id` + Please pass the id of the object by calling `.id`. MSG end @@ -162,11 +190,13 @@ module ActiveRecord } record = statement.execute([id], self, connection).first unless record - raise RecordNotFound, "Couldn't find #{name} with '#{primary_key}'=#{id}" + raise RecordNotFound.new("Couldn't find #{name} with '#{primary_key}'=#{id}", + name, primary_key, id) end record - rescue RangeError - raise RecordNotFound, "Couldn't find #{name} with an out of range value for '#{primary_key}'" + rescue ::RangeError + raise RecordNotFound.new("Couldn't find #{name} with an out of range value for '#{primary_key}'", + name, primary_key) end def find_by(*args) # :nodoc: @@ -175,7 +205,7 @@ module ActiveRecord hash = args.first return super if hash.values.any? { |v| - v.nil? || Array === v || Hash === v + v.nil? || Array === v || Hash === v || Relation === v } # We can't cache Post.find_by(author: david) ...yet @@ -191,15 +221,15 @@ module ActiveRecord } begin statement.execute(hash.values, self, connection).first - rescue TypeError => e - raise ActiveRecord::StatementInvalid.new(e.message, e) - rescue RangeError + rescue TypeError + raise ActiveRecord::StatementInvalid + rescue ::RangeError nil end end def find_by!(*args) # :nodoc: - find_by(*args) or raise RecordNotFound.new("Couldn't find #{name}") + find_by(*args) || raise(RecordNotFound.new("Couldn't find #{name}", name)) end def initialize_generated_modules # :nodoc: @@ -223,7 +253,7 @@ module ActiveRecord elsif !connected? "#{super} (call '#{super}.connection' to establish a connection)" elsif table_exists? - attr_list = attribute_types.map { |name, type| "#{name}: #{type.type}" } * ', ' + attr_list = attribute_types.map { |name, type| "#{name}: #{type.type}" } * ", " "#{super}(#{attr_list})" else "#{super}(Table doesn't exist)" @@ -238,7 +268,7 @@ module ActiveRecord # Returns an instance of <tt>Arel::Table</tt> loaded with the current table name. # # class Post < ActiveRecord::Base - # scope :published_and_commented, -> { published.and(self.arel_table[:comments_count].gt(0)) } + # scope :published_and_commented, -> { published.and(arel_table[:comments_count].gt(0)) } # end def arel_table # :nodoc: @arel_table ||= Arel::Table.new(table_name, type_caster: type_caster) @@ -247,13 +277,18 @@ module ActiveRecord # Returns the Arel engine. def arel_engine # :nodoc: @arel_engine ||= - if Base == self || connection_handler.retrieve_connection_pool(self) + if Base == self || connection_handler.retrieve_connection_pool(connection_specification_name) self else superclass.arel_engine end end + def arel_attribute(name, table = arel_table) # :nodoc: + name = attribute_alias(name) if attribute_alias?(name) + table[name] + end + def predicate_builder # :nodoc: @predicate_builder ||= PredicateBuilder.new(table_metadata) end @@ -264,25 +299,26 @@ module ActiveRecord private - def cached_find_by_statement(key, &block) # :nodoc: - @find_by_statement_cache[key] || @find_by_statement_cache.synchronize { - @find_by_statement_cache[key] ||= StatementCache.create(connection, &block) - } - end + def cached_find_by_statement(key, &block) + cache = @find_by_statement_cache[connection.prepared_statements] + cache[key] || cache.synchronize { + cache[key] ||= StatementCache.create(connection, &block) + } + end - def relation # :nodoc: - relation = Relation.create(self, arel_table, predicate_builder) + def relation + relation = Relation.create(self, arel_table, predicate_builder) - if finder_needs_type_condition? - relation.where(type_condition).create_with(inheritance_column.to_sym => sti_name) - else - relation + if finder_needs_type_condition? && !ignore_default_scope? + relation.where(type_condition).create_with(inheritance_column.to_sym => sti_name) + else + relation + end end - end - def table_metadata # :nodoc: - TableMetadata.new(self, arel_table) - end + def table_metadata + TableMetadata.new(self, arel_table) + end end # New objects can be instantiated as either empty (pass no construction parameter) or pre-set with @@ -294,8 +330,8 @@ module ActiveRecord # # Instantiates a single new object # User.new(first_name: 'Jamie') def initialize(attributes = nil) - @attributes = self.class._default_attributes.dup self.class.define_attribute_methods + @attributes = self.class._default_attributes.deep_dup init_internals initialize_internals_callback @@ -303,12 +339,12 @@ module ActiveRecord assign_attributes(attributes) if attributes yield self if block_given? - run_callbacks :initialize + _run_initialize_callbacks end # Initialize an empty model object from +coder+. +coder+ should be # the result of previously encoding an Active Record model, using - # `encode_with` + # #encode_with. # # class Post < ActiveRecord::Base # end @@ -322,16 +358,18 @@ module ActiveRecord # post.title # => 'hello world' def init_with(coder) coder = LegacyYamlAdapter.convert(self.class, coder) - @attributes = coder['attributes'] + @attributes = self.class.yaml_encoder.decode(coder) init_internals - @new_record = coder['new_record'] + @new_record = coder["new_record"] self.class.define_attribute_methods - run_callbacks :find - run_callbacks :initialize + yield self if block_given? + + _run_find_callbacks + _run_initialize_callbacks self end @@ -364,10 +402,10 @@ module ActiveRecord ## def initialize_dup(other) # :nodoc: - @attributes = @attributes.dup + @attributes = @attributes.deep_dup @attributes.reset(self.class.primary_key) - run_callbacks(:initialize) + _run_initialize_callbacks @new_record = true @destroyed = false @@ -377,7 +415,7 @@ module ActiveRecord # Populate +coder+ with attributes about this record that should be # serialized. The structure of +coder+ defined in this method is - # guaranteed to match the structure of +coder+ passed to the +init_with+ + # guaranteed to match the structure of +coder+ passed to the #init_with # method. # # Example: @@ -388,11 +426,9 @@ module ActiveRecord # Post.new.encode_with(coder) # coder # => {"attributes" => {"id" => nil, ... }} def encode_with(coder) - # FIXME: Remove this when we better serialize attributes - coder['raw_attributes'] = attributes_before_type_cast - coder['attributes'] = @attributes - coder['new_record'] = new_record? - coder['active_record_yaml_version'] = 1 + self.class.yaml_encoder.encode(@attributes, coder) + coder["new_record"] = new_record? + coder["active_record_yaml_version"] = 2 end # Returns true if +comparison_object+ is the same exact object, or +comparison_object+ @@ -416,7 +452,7 @@ module ActiveRecord # [ Person.find(1), Person.find(2), Person.find(3) ] & [ Person.find(1), Person.find(4) ] # => [ Person.find(1) ] def hash if id - id.hash + self.class.hash ^ self.id.hash else super end @@ -464,82 +500,83 @@ module ActiveRecord # We check defined?(@attributes) not to issue warnings if the object is # allocated but not initialized. inspection = if defined?(@attributes) && @attributes - self.class.column_names.collect { |name| - if has_attribute?(name) - "#{name}: #{attribute_for_inspect(name)}" - end - }.compact.join(", ") - else - "not initialized" - end + self.class.attribute_names.collect do |name| + if has_attribute?(name) + "#{name}: #{attribute_for_inspect(name)}" + end + end.compact.join(", ") + else + "not initialized" + end + "#<#{self.class} #{inspection}>" end - # Takes a PP and prettily prints this record to it, allowing you to get a nice result from `pp record` + # Takes a PP and prettily prints this record to it, allowing you to get a nice result from <tt>pp record</tt> # when pp is required. def pretty_print(pp) return super if custom_inspect_method_defined? pp.object_address_group(self) do if defined?(@attributes) && @attributes column_names = self.class.column_names.select { |name| has_attribute?(name) || new_record? } - pp.seplist(column_names, proc { pp.text ',' }) do |column_name| + pp.seplist(column_names, proc { pp.text "," }) do |column_name| column_value = read_attribute(column_name) - pp.breakable ' ' + pp.breakable " " pp.group(1) do pp.text column_name - pp.text ':' + pp.text ":" pp.breakable pp.pp column_value end end else - pp.breakable ' ' - pp.text 'not initialized' + pp.breakable " " + pp.text "not initialized" end end end # Returns a hash of the given methods with their names as keys and returned values as values. def slice(*methods) - Hash[methods.map! { |method| [method, public_send(method)] }].with_indifferent_access + Hash[methods.flatten.map! { |method| [method, public_send(method)] }].with_indifferent_access end private - # Under Ruby 1.9, Array#flatten will call #to_ary (recursively) on each of the elements - # of the array, and then rescues from the possible NoMethodError. If those elements are - # ActiveRecord::Base's, then this triggers the various method_missing's that we have, - # which significantly impacts upon performance. - # - # So we can avoid the method_missing hit by explicitly defining #to_ary as nil here. - # - # See also http://tenderlovemaking.com/2011/06/28/til-its-ok-to-return-nil-from-to_ary.html - def to_ary # :nodoc: - nil - end + # +Array#flatten+ will call +#to_ary+ (recursively) on each of the elements of + # the array, and then rescues from the possible +NoMethodError+. If those elements are + # +ActiveRecord::Base+'s, then this triggers the various +method_missing+'s that we have, + # which significantly impacts upon performance. + # + # So we can avoid the +method_missing+ hit by explicitly defining +#to_ary+ as +nil+ here. + # + # See also http://tenderlovemaking.com/2011/06/28/til-its-ok-to-return-nil-from-to_ary.html + def to_ary + nil + end - def init_internals - @readonly = false - @destroyed = false - @marked_for_destruction = false - @destroyed_by_association = nil - @new_record = true - @txn = nil - @_start_transaction_state = {} - @transaction_state = nil - end + def init_internals + @readonly = false + @destroyed = false + @marked_for_destruction = false + @destroyed_by_association = nil + @new_record = true + @txn = nil + @_start_transaction_state = {} + @transaction_state = nil + end - def initialize_internals_callback - end + def initialize_internals_callback + end - def thaw - if frozen? - @attributes = @attributes.dup + def thaw + if frozen? + @attributes = @attributes.dup + end end - end - def custom_inspect_method_defined? - self.class.instance_method(:inspect).owner != ActiveRecord::Base.instance_method(:inspect).owner - end + def custom_inspect_method_defined? + self.class.instance_method(:inspect).owner != ActiveRecord::Base.instance_method(:inspect).owner + end end end diff --git a/activerecord/lib/active_record/counter_cache.rb b/activerecord/lib/active_record/counter_cache.rb index 82596b63df..e2da512813 100644 --- a/activerecord/lib/active_record/counter_cache.rb +++ b/activerecord/lib/active_record/counter_cache.rb @@ -26,7 +26,7 @@ module ActiveRecord has_many_association = has_many.find { |association| association.counter_cache_column && association.counter_cache_column.to_sym == counter_association.to_sym } counter_association = has_many_association.plural_name if has_many_association end - raise ArgumentError, "'#{self.name}' has no association called '#{counter_association}'" unless has_many_association + raise ArgumentError, "'#{name}' has no association called '#{counter_association}'" unless has_many_association if has_many_association.is_a? ActiveRecord::Reflection::ThroughReflection has_many_association = has_many_association.through_reflection @@ -45,14 +45,14 @@ module ActiveRecord end # A generic "counter updater" implementation, intended primarily to be - # used by increment_counter and decrement_counter, but which may also + # used by #increment_counter and #decrement_counter, but which may also # be useful on its own. It simply does a direct SQL update for the record # with the given ID, altering the given hash of counters by the amount # given by the corresponding value: # # ==== Parameters # - # * +id+ - The id of the object you wish to update a counter on or an Array of ids. + # * +id+ - The id of the object you wish to update a counter on or an array of ids. # * +counters+ - A Hash containing the names of the fields # to update as keys and the amount to update the field by as values. # @@ -75,48 +75,48 @@ module ActiveRecord # # WHERE id IN (10, 15) def update_counters(id, counters) updates = counters.map do |counter_name, value| - operator = value < 0 ? '-' : '+' + operator = value < 0 ? "-" : "+" quoted_column = connection.quote_column_name(counter_name) "#{quoted_column} = COALESCE(#{quoted_column}, 0) #{operator} #{value.abs}" end - unscoped.where(primary_key => id).update_all updates.join(', ') + unscoped.where(primary_key => id).update_all updates.join(", ") end # Increment a numeric field by one, via a direct SQL update. # # This method is used primarily for maintaining counter_cache columns that are - # used to store aggregate values. For example, a DiscussionBoard may cache + # used to store aggregate values. For example, a +DiscussionBoard+ may cache # posts_count and comments_count to avoid running an SQL query to calculate the # number of posts and comments there are, each time it is displayed. # # ==== Parameters # # * +counter_name+ - The name of the field that should be incremented. - # * +id+ - The id of the object that should be incremented or an Array of ids. + # * +id+ - The id of the object that should be incremented or an array of ids. # # ==== Examples # - # # Increment the post_count column for the record with an id of 5 - # DiscussionBoard.increment_counter(:post_count, 5) + # # Increment the posts_count column for the record with an id of 5 + # DiscussionBoard.increment_counter(:posts_count, 5) def increment_counter(counter_name, id) update_counters(id, counter_name => 1) end # Decrement a numeric field by one, via a direct SQL update. # - # This works the same as increment_counter but reduces the column value by + # This works the same as #increment_counter but reduces the column value by # 1 instead of increasing it. # # ==== Parameters # # * +counter_name+ - The name of the field that should be decremented. - # * +id+ - The id of the object that should be decremented or an Array of ids. + # * +id+ - The id of the object that should be decremented or an array of ids. # # ==== Examples # - # # Decrement the post_count column for the record with an id of 5 - # DiscussionBoard.decrement_counter(:post_count, 5) + # # Decrement the posts_count column for the record with an id of 5 + # DiscussionBoard.decrement_counter(:posts_count, 5) def decrement_counter(counter_name, id) update_counters(id, counter_name => -1) end @@ -159,6 +159,5 @@ module ActiveRecord yield association(name.to_sym) if reflection.belongs_to? && reflection.counter_cache_column end end - end end diff --git a/activerecord/lib/active_record/define_callbacks.rb b/activerecord/lib/active_record/define_callbacks.rb new file mode 100644 index 0000000000..7d955a24be --- /dev/null +++ b/activerecord/lib/active_record/define_callbacks.rb @@ -0,0 +1,20 @@ +module ActiveRecord + # This module exists because `ActiveRecord::AttributeMethods::Dirty` needs to + # define callbacks, but continue to have its version of `save` be the super + # method of `ActiveRecord::Callbacks`. This will be removed when the removal + # of deprecated code removes this need. + module DefineCallbacks + extend ActiveSupport::Concern + + module ClassMethods # :nodoc: + include ActiveModel::Callbacks + end + + included do + include ActiveModel::Validations::Callbacks + + define_model_callbacks :initialize, :find, :touch, only: :after + define_model_callbacks :save, :create, :update, :destroy + end + end +end diff --git a/activerecord/lib/active_record/dynamic_matchers.rb b/activerecord/lib/active_record/dynamic_matchers.rb index b6dd6814db..08d42f3dd4 100644 --- a/activerecord/lib/active_record/dynamic_matchers.rb +++ b/activerecord/lib/active_record/dynamic_matchers.rb @@ -1,6 +1,7 @@ + module ActiveRecord module DynamicMatchers #:nodoc: - def respond_to?(name, include_private = false) + def respond_to_missing?(name, include_private = false) if self == Base super else @@ -11,111 +12,111 @@ module ActiveRecord private - def method_missing(name, *arguments, &block) - match = Method.match(self, name) + def method_missing(name, *arguments, &block) + match = Method.match(self, name) - if match && match.valid? - match.define - send(name, *arguments, &block) - else - super + if match && match.valid? + match.define + send(name, *arguments, &block) + else + super + end end - end - class Method - @matchers = [] + class Method + @matchers = [] - class << self - attr_reader :matchers + class << self + attr_reader :matchers - def match(model, name) - klass = matchers.find { |k| name =~ k.pattern } - klass.new(model, name) if klass - end + def match(model, name) + klass = matchers.find { |k| k.pattern.match?(name) } + klass.new(model, name) if klass + end - def pattern - @pattern ||= /\A#{prefix}_([_a-zA-Z]\w*)#{suffix}\Z/ - end + def pattern + @pattern ||= /\A#{prefix}_([_a-zA-Z]\w*)#{suffix}\Z/ + end - def prefix - raise NotImplementedError - end + def prefix + raise NotImplementedError + end - def suffix - '' + def suffix + "" + end end - end - attr_reader :model, :name, :attribute_names + attr_reader :model, :name, :attribute_names - def initialize(model, name) - @model = model - @name = name.to_s - @attribute_names = @name.match(self.class.pattern)[1].split('_and_') - @attribute_names.map! { |n| @model.attribute_aliases[n] || n } - end + def initialize(model, name) + @model = model + @name = name.to_s + @attribute_names = @name.match(self.class.pattern)[1].split("_and_") + @attribute_names.map! { |n| @model.attribute_aliases[n] || n } + end - def valid? - attribute_names.all? { |name| model.columns_hash[name] || model.reflect_on_aggregation(name.to_sym) } - end + def valid? + attribute_names.all? { |name| model.columns_hash[name] || model.reflect_on_aggregation(name.to_sym) } + end - def define - model.class_eval <<-CODE, __FILE__, __LINE__ + 1 - def self.#{name}(#{signature}) - #{body} - end - CODE - end + def define + model.class_eval <<-CODE, __FILE__, __LINE__ + 1 + def self.#{name}(#{signature}) + #{body} + end + CODE + end - private + private - def body - "#{finder}(#{attributes_hash})" - end + def body + "#{finder}(#{attributes_hash})" + end - # The parameters in the signature may have reserved Ruby words, in order - # to prevent errors, we start each param name with `_`. - def signature - attribute_names.map { |name| "_#{name}" }.join(', ') - end + # The parameters in the signature may have reserved Ruby words, in order + # to prevent errors, we start each param name with `_`. + def signature + attribute_names.map { |name| "_#{name}" }.join(", ") + end - # Given that the parameters starts with `_`, the finder needs to use the - # same parameter name. - def attributes_hash - "{" + attribute_names.map { |name| ":#{name} => _#{name}" }.join(',') + "}" - end + # Given that the parameters starts with `_`, the finder needs to use the + # same parameter name. + def attributes_hash + "{" + attribute_names.map { |name| ":#{name} => _#{name}" }.join(",") + "}" + end - def finder - raise NotImplementedError + def finder + raise NotImplementedError + end end - end - class FindBy < Method - Method.matchers << self + class FindBy < Method + Method.matchers << self - def self.prefix - "find_by" - end + def self.prefix + "find_by" + end - def finder - "find_by" + def finder + "find_by" + end end - end - class FindByBang < Method - Method.matchers << self + class FindByBang < Method + Method.matchers << self - def self.prefix - "find_by" - end + def self.prefix + "find_by" + end - def self.suffix - "!" - end + def self.suffix + "!" + end - def finder - "find_by!" + def finder + "find_by!" + end end - end end end diff --git a/activerecord/lib/active_record/enum.rb b/activerecord/lib/active_record/enum.rb index c0d9d9c1c8..0ab03b2ab3 100644 --- a/activerecord/lib/active_record/enum.rb +++ b/activerecord/lib/active_record/enum.rb @@ -1,4 +1,4 @@ -require 'active_support/core_ext/object/deep_dup' +require "active_support/core_ext/object/deep_dup" module ActiveRecord # Declare an enum attribute where the values map to integers in the database, @@ -18,10 +18,9 @@ module ActiveRecord # conversation.archived? # => true # conversation.status # => "archived" # - # # conversation.update! status: 1 + # # conversation.status = 1 # conversation.status = "archived" # - # # conversation.update! status: nil # conversation.status = nil # conversation.status.nil? # => true # conversation.status # => nil @@ -47,13 +46,13 @@ module ActiveRecord # Good practice is to let the first declared status be the default. # # Finally, it's also possible to explicitly map the relation between attribute and - # database integer with a +Hash+: + # database integer with a hash: # # class Conversation < ActiveRecord::Base # enum status: { active: 0, archived: 1 } # end # - # Note that when an +Array+ is used, the implicit mapping from the values to database + # Note that when an array is used, the implicit mapping from the values to database # integers is derived from the order the values appear in the array. In the example, # <tt>:active</tt> is mapped to +0+ as it's the first element, and <tt>:archived</tt> # is mapped to +1+. In general, the +i+-th element is mapped to <tt>i-1</tt> in the @@ -61,7 +60,7 @@ module ActiveRecord # # Therefore, once a value is added to the enum array, its position in the array must # be maintained, and new values should only be added to the end of the array. To - # remove unused values, the explicit +Hash+ syntax should be used. + # remove unused values, the explicit hash syntax should be used. # # In rare circumstances you might need to access the mapping directly. # The mappings are exposed through a class method with the pluralized attribute @@ -75,26 +74,28 @@ module ActiveRecord # # Conversation.where("status <> ?", Conversation.statuses[:archived]) # - # You can use the +:enum_prefix+ or +:enum_suffix+ options when you need - # to define multiple enums with same values. If the passed value is +true+, - # the methods are prefixed/suffixed with the name of the enum. + # You can use the +:_prefix+ or +:_suffix+ options when you need to define + # multiple enums with same values. If the passed value is +true+, the methods + # are prefixed/suffixed with the name of the enum. It is also possible to + # supply a custom value: # - # class Invoice < ActiveRecord::Base - # enum verification: [:done, :fail], enum_prefix: true + # class Conversation < ActiveRecord::Base + # enum status: [:active, :archived], _suffix: true + # enum comments_status: [:active, :inactive], _prefix: :comments # end # - # It is also possible to supply a custom prefix. + # With the above example, the bang and predicate methods along with the + # associated scopes are now prefixed and/or suffixed accordingly: # - # class Invoice < ActiveRecord::Base - # enum verification: [:done, :fail], enum_prefix: :verification_status - # end + # conversation.active_status! + # conversation.archived_status? # => false # - # Note that <tt>:enum_prefix</tt>/<tt>:enum_suffix</tt> are reserved keywords - # and can not be used as an enum name. + # conversation.comments_inactive! + # conversation.comments_active? # => false module Enum def self.extended(base) # :nodoc: - base.class_attribute(:defined_enums) + base.class_attribute(:defined_enums, instance_writer: false) base.defined_enums = {} end @@ -103,10 +104,13 @@ module ActiveRecord super end - class EnumType < Type::Value - def initialize(name, mapping) + class EnumType < Type::Value # :nodoc: + delegate :type, to: :subtype + + def initialize(name, mapping, subtype) @name = name @mapping = mapping + @subtype = subtype end def cast(value) @@ -117,41 +121,52 @@ module ActiveRecord elsif mapping.has_value?(value) mapping.key(value) else - raise ArgumentError, "'#{value}' is not a valid #{name}" + assert_valid_value(value) end end def deserialize(value) return if value.nil? - mapping.key(value.to_i) + mapping.key(subtype.deserialize(value)) end def serialize(value) mapping.fetch(value, value) end + def assert_valid_value(value) + unless value.blank? || mapping.has_key?(value) || mapping.has_value?(value) + raise ArgumentError, "'#{value}' is not a valid #{name}" + end + end + + # TODO Change this to private once we've dropped Ruby 2.2 support. + # Workaround for Ruby 2.2 "private attribute?" warning. protected - attr_reader :name, :mapping + attr_reader :name, :mapping, :subtype end def enum(definitions) klass = self - enum_prefix = definitions.delete(:enum_prefix) - enum_suffix = definitions.delete(:enum_suffix) + enum_prefix = definitions.delete(:_prefix) + enum_suffix = definitions.delete(:_suffix) definitions.each do |name, values| # statuses = { } enum_values = ActiveSupport::HashWithIndifferentAccess.new name = name.to_sym - # def self.statuses statuses end + # def self.statuses() statuses end detect_enum_conflict!(name, name.to_s.pluralize, true) klass.singleton_class.send(:define_method, name.to_s.pluralize) { enum_values } detect_enum_conflict!(name, name) detect_enum_conflict!(name, "#{name}=") - attribute name, EnumType.new(name, enum_values) + attr = attribute_alias?(name) ? attribute_alias(name) : name + decorate_attribute_type(attr, :enum) do |subtype| + EnumType.new(attr, enum_values, subtype) + end _enum_methods_module.module_eval do pairs = values.respond_to?(:each_pair) ? values.each_pair : values.each_with_index @@ -172,15 +187,15 @@ module ActiveRecord # def active?() status == 0 end klass.send(:detect_enum_conflict!, name, "#{value_method_name}?") - define_method("#{value_method_name}?") { self[name] == value.to_s } + define_method("#{value_method_name}?") { self[attr] == value.to_s } # def active!() update! status: :active end klass.send(:detect_enum_conflict!, name, "#{value_method_name}!") - define_method("#{value_method_name}!") { update! name => value } + define_method("#{value_method_name}!") { update!(attr => value) } # scope :active, -> { where status: 0 } klass.send(:detect_enum_conflict!, name, value_method_name, true) - klass.scope value_method_name, -> { klass.where name => value } + klass.scope value_method_name, -> { where(attr => value) } end end defined_enums[name.to_s] = enum_values @@ -203,30 +218,22 @@ module ActiveRecord def detect_enum_conflict!(enum_name, method_name, klass_method = false) if klass_method && dangerous_class_method?(method_name) - raise ArgumentError, ENUM_CONFLICT_MESSAGE % { - enum: enum_name, - klass: self.name, - type: 'class', - method: method_name, - source: 'Active Record' - } + raise_conflict_error(enum_name, method_name, type: "class") elsif !klass_method && dangerous_attribute_method?(method_name) - raise ArgumentError, ENUM_CONFLICT_MESSAGE % { - enum: enum_name, - klass: self.name, - type: 'instance', - method: method_name, - source: 'Active Record' - } + raise_conflict_error(enum_name, method_name) elsif !klass_method && method_defined_within?(method_name, _enum_methods_module, Module) - raise ArgumentError, ENUM_CONFLICT_MESSAGE % { - enum: enum_name, - klass: self.name, - type: 'instance', - method: method_name, - source: 'another enum' - } + raise_conflict_error(enum_name, method_name, source: "another enum") end end + + def raise_conflict_error(enum_name, method_name, type: "instance", source: "Active Record") + raise ArgumentError, ENUM_CONFLICT_MESSAGE % { + enum: enum_name, + klass: name, + type: type, + method: method_name, + source: source + } + end end end diff --git a/activerecord/lib/active_record/errors.rb b/activerecord/lib/active_record/errors.rb index 0f1759abaa..18fac5af1b 100644 --- a/activerecord/lib/active_record/errors.rb +++ b/activerecord/lib/active_record/errors.rb @@ -1,5 +1,4 @@ module ActiveRecord - # = Active Record Errors # # Generic Active Record exception class. @@ -7,8 +6,10 @@ module ActiveRecord end # Raised when the single-table inheritance mechanism fails to locate the subclass - # (for example due to improper usage of column that +inheritance_column+ points to). - class SubclassNotFound < ActiveRecordError #:nodoc: + # (for example due to improper usage of column that + # {ActiveRecord::Base.inheritance_column}[rdoc-ref:ModelSchema::ClassMethods#inheritance_column] + # points to). + class SubclassNotFound < ActiveRecordError end # Raised when an object assigned to an association has an incorrect type. @@ -40,27 +41,40 @@ module ActiveRecord class AdapterNotFound < ActiveRecordError end - # Raised when connection to the database could not been established (for - # example when +connection=+ is given a nil object). + # Raised when connection to the database could not been established (for example when + # {ActiveRecord::Base.connection=}[rdoc-ref:ConnectionHandling#connection] + # is given a +nil+ object). class ConnectionNotEstablished < ActiveRecordError end - # Raised when Active Record cannot find record by given id or set of ids. + # Raised when Active Record cannot find a record by given id or set of ids. class RecordNotFound < ActiveRecordError + attr_reader :model, :primary_key, :id + + def initialize(message = nil, model = nil, primary_key = nil, id = nil) + @primary_key = primary_key + @model = model + @id = id + + super(message) + end end - # Raised by ActiveRecord::Base.save! and ActiveRecord::Base.create! methods when record cannot be - # saved because record is invalid. + # Raised by {ActiveRecord::Base#save!}[rdoc-ref:Persistence#save!] and + # {ActiveRecord::Base.create!}[rdoc-ref:Persistence::ClassMethods#create!] + # methods when a record is invalid and can not be saved. class RecordNotSaved < ActiveRecordError attr_reader :record - def initialize(message, record = nil) + def initialize(message = nil, record = nil) @record = record super(message) end end - # Raised by ActiveRecord::Base.destroy! when a call to destroy would return false. + # Raised by {ActiveRecord::Base#destroy!}[rdoc-ref:Persistence#destroy!] + # when a call to {#destroy}[rdoc-ref:Persistence#destroy!] + # would return false. # # begin # complex_operation_that_internally_calls_destroy! @@ -71,7 +85,7 @@ module ActiveRecord class RecordNotDestroyed < ActiveRecordError attr_reader :record - def initialize(message, record = nil) + def initialize(message = nil, record = nil) @record = record super(message) end @@ -79,18 +93,15 @@ module ActiveRecord # Superclass for all database execution errors. # - # Wraps the underlying database error as +original_exception+. + # Wraps the underlying database error as +cause+. class StatementInvalid < ActiveRecordError - attr_reader :original_exception - - def initialize(message, original_exception = nil) - super(message) - @original_exception = original_exception + def initialize(message = nil) + super(message || $!.try(:message)) end end # Defunct wrapper class kept for compatibility. - # +StatementInvalid+ wraps the original exception now. + # StatementInvalid wraps the original exception now. class WrappedDatabaseException < StatementInvalid end @@ -102,9 +113,49 @@ module ActiveRecord class InvalidForeignKey < WrappedDatabaseException end + # Raised when a foreign key constraint cannot be added because the column type does not match the referenced column type. + class MismatchedForeignKey < StatementInvalid + def initialize(adapter = nil, message: nil, table: nil, foreign_key: nil, target_table: nil, primary_key: nil) + @adapter = adapter + if table + msg = <<-EOM.strip_heredoc + Column `#{foreign_key}` on table `#{table}` has a type of `#{column_type(table, foreign_key)}`. + This does not match column `#{primary_key}` on `#{target_table}`, which has type `#{column_type(target_table, primary_key)}`. + To resolve this issue, change the type of the `#{foreign_key}` column on `#{table}` to be :integer. (For example `t.integer #{foreign_key}`). + EOM + else + msg = <<-EOM + There is a mismatch between the foreign key and primary key column types. + Verify that the foreign key column type and the primary key of the associated table match types. + EOM + end + if message + msg << "\nOriginal message: #{message}" + end + super(msg) + end + + private + def column_type(table, column) + @adapter.columns(table).detect { |c| c.name == column }.sql_type + end + end + + # Raised when a record cannot be inserted or updated because it would violate a not null constraint. + class NotNullViolation < StatementInvalid + end + + # Raised when a record cannot be inserted or updated because a value too long for a column type. + class ValueTooLong < StatementInvalid + end + + # Raised when values that executed are out of range. + class RangeError < StatementInvalid + end + # Raised when number of bind variables in statement given to +:condition+ key - # (for example, when using +find+ method) does not match number of expected - # values supplied. + # (for example, when using {ActiveRecord::Base.find}[rdoc-ref:FinderMethods#find] method) + # does not match number of expected values supplied. # # For example, when there are two placeholders with only one value supplied: # @@ -116,6 +167,11 @@ module ActiveRecord class NoDatabaseError < StatementInvalid end + # Raised when Postgres returns 'cached plan must not change result type' and + # we cannot retry gracefully (e.g. inside a transaction) + class PreparedStatementCacheExpired < StatementInvalid + end + # Raised on attempt to save stale record. Record is stale when it's being saved in another query after # instantiation, for example, when two users edit the same wiki page and one starts editing and saves # the page before the other. @@ -125,16 +181,21 @@ module ActiveRecord class StaleObjectError < ActiveRecordError attr_reader :record, :attempted_action - def initialize(record, attempted_action) - super("Attempted to #{attempted_action} a stale object: #{record.class.name}") - @record = record - @attempted_action = attempted_action + def initialize(record = nil, attempted_action = nil) + if record && attempted_action + @record = record + @attempted_action = attempted_action + super("Attempted to #{attempted_action} a stale object: #{record.class.name}.") + else + super("Stale object error.") + end end - end # Raised when association is being configured improperly or user tries to use - # offset and limit together with +has_many+ or +has_and_belongs_to_many+ + # offset and limit together with + # {ActiveRecord::Base.has_many}[rdoc-ref:Associations::ClassMethods#has_many] or + # {ActiveRecord::Base.has_and_belongs_to_many}[rdoc-ref:Associations::ClassMethods#has_and_belongs_to_many] # associations. class ConfigurationError < ActiveRecordError end @@ -143,9 +204,10 @@ module ActiveRecord class ReadOnlyRecord < ActiveRecordError end - # ActiveRecord::Transactions::ClassMethods.transaction uses this exception - # to distinguish a deliberate rollback from other exceptional situations. - # Normally, raising an exception will cause the +transaction+ method to rollback + # {ActiveRecord::Base.transaction}[rdoc-ref:Transactions::ClassMethods#transaction] + # uses this exception to distinguish a deliberate rollback from other exceptional situations. + # Normally, raising an exception will cause the + # {.transaction}[rdoc-ref:Transactions::ClassMethods#transaction] method to rollback # the database transaction *and* pass on the exception. But if you raise an # ActiveRecord::Rollback exception, then the database transaction will be rolled back, # without passing on the exception. @@ -182,25 +244,26 @@ module ActiveRecord UnknownAttributeError = ActiveModel::UnknownAttributeError # Raised when an error occurred while doing a mass assignment to an attribute through the - # +attributes=+ method. The exception has an +attribute+ property that is the name of the - # offending attribute. + # {ActiveRecord::Base#attributes=}[rdoc-ref:AttributeAssignment#attributes=] method. + # The exception has an +attribute+ property that is the name of the offending attribute. class AttributeAssignmentError < ActiveRecordError attr_reader :exception, :attribute - def initialize(message, exception, attribute) + def initialize(message = nil, exception = nil, attribute = nil) super(message) @exception = exception @attribute = attribute end end - # Raised when there are multiple errors while doing a mass assignment through the +attributes+ + # Raised when there are multiple errors while doing a mass assignment through the + # {ActiveRecord::Base#attributes=}[rdoc-ref:AttributeAssignment#attributes=] # method. The exception has an +errors+ property that contains an array of AttributeAssignmentError # objects, each corresponding to the error while assigning to an attribute. class MultiparameterAssignmentErrors < ActiveRecordError attr_reader :errors - def initialize(errors) + def initialize(errors = nil) @errors = errors end end @@ -209,11 +272,16 @@ module ActiveRecord class UnknownPrimaryKey < ActiveRecordError attr_reader :model - def initialize(model) - super("Unknown primary key for table #{model.table_name} in model #{model}.") - @model = model + def initialize(model = nil, description = nil) + if model + message = "Unknown primary key for table #{model.table_name} in model #{model}." + message += "\n#{description}" if description + @model = model + super(message) + else + super("Unknown primary key.") + end end - end # Raised when a relation cannot be mutated because it's already loaded. @@ -236,7 +304,32 @@ module ActiveRecord # * 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. + # The mysql2 and postgresql adapters support setting the transaction isolation level. class TransactionIsolationError < ActiveRecordError end + + # TransactionRollbackError will be raised when a transaction is rolled + # back by the database due to a serialization failure or a deadlock. + # + # See the following: + # + # * http://www.postgresql.org/docs/current/static/transaction-iso.html + # * https://dev.mysql.com/doc/refman/5.7/en/error-messages-server.html#error_er_lock_deadlock + class TransactionRollbackError < StatementInvalid + end + + # SerializationFailure will be raised when a transaction is rolled + # back by the database due to a serialization failure. + class SerializationFailure < TransactionRollbackError + end + + # Deadlocked will be raised when a transaction is rolled + # back by the database when a deadlock is encountered. + class Deadlocked < TransactionRollbackError + end + + # IrreversibleOrderError is raised when a relation's order is too complex for + # +reverse_order+ to automatically reverse. + class IrreversibleOrderError < ActiveRecordError + end end diff --git a/activerecord/lib/active_record/explain.rb b/activerecord/lib/active_record/explain.rb index 727a9befc1..8f7ae2c33c 100644 --- a/activerecord/lib/active_record/explain.rb +++ b/activerecord/lib/active_record/explain.rb @@ -1,5 +1,4 @@ -require 'active_support/lazy_load_hooks' -require 'active_record/explain_registry' +require "active_record/explain_registry" module ActiveRecord module Explain @@ -16,15 +15,14 @@ module ActiveRecord # Makes the adapter execute EXPLAIN for the tuples of queries and bindings. # Returns a formatted string ready to be logged. def exec_explain(queries) # :nodoc: - str = queries.map do |sql, bind| - [].tap do |msg| - msg << "EXPLAIN for: #{sql}" - unless bind.empty? - bind_msg = bind.map {|col, val| [col.name, val]}.inspect - msg.last << " #{bind_msg}" - end - msg << connection.explain(sql, bind) - end.join("\n") + str = queries.map do |sql, binds| + msg = "EXPLAIN for: #{sql}" + unless binds.empty? + msg << " " + msg << binds.map { |attr| render_bind(attr) }.inspect + end + msg << "\n" + msg << connection.explain(sql, binds) end.join("\n") # Overriding inspect to be more human readable, especially in the console. @@ -34,5 +32,17 @@ module ActiveRecord str end + + private + + def render_bind(attr) + value = if attr.type.binary? && attr.value + "<#{attr.value_for_database.to_s.bytesize} bytes of binary data>" + else + connection.type_cast(attr.value_for_database) + end + + [attr.name, value] + end end end diff --git a/activerecord/lib/active_record/explain_registry.rb b/activerecord/lib/active_record/explain_registry.rb index f5cd57e075..ef1ce3dc85 100644 --- a/activerecord/lib/active_record/explain_registry.rb +++ b/activerecord/lib/active_record/explain_registry.rb @@ -1,4 +1,4 @@ -require 'active_support/per_thread_registry' +require "active_support/per_thread_registry" module ActiveRecord # This is a thread locals registry for EXPLAIN. For example @@ -7,7 +7,7 @@ module ActiveRecord # # returns the collected queries local to the current thread. # - # See the documentation of <tt>ActiveSupport::PerThreadRegistry</tt> + # See the documentation of ActiveSupport::PerThreadRegistry # for further details. class ExplainRegistry # :nodoc: extend ActiveSupport::PerThreadRegistry diff --git a/activerecord/lib/active_record/explain_subscriber.rb b/activerecord/lib/active_record/explain_subscriber.rb index 9adabd7819..abd8cfc8f2 100644 --- a/activerecord/lib/active_record/explain_subscriber.rb +++ b/activerecord/lib/active_record/explain_subscriber.rb @@ -1,5 +1,5 @@ -require 'active_support/notifications' -require 'active_record/explain_registry' +require "active_support/notifications" +require "active_record/explain_registry" module ActiveRecord class ExplainSubscriber # :nodoc: @@ -14,14 +14,17 @@ module ActiveRecord end # SCHEMA queries cannot be EXPLAINed, also we do not want to run EXPLAIN on - # our own EXPLAINs now matter how loopingly beautiful that would be. + # our own EXPLAINs no matter how loopingly beautiful that would be. # # On the other hand, we want to monitor the performance of our real database # queries, not the performance of the access to the query cache. - IGNORED_PAYLOADS = %w(SCHEMA EXPLAIN CACHE) + IGNORED_PAYLOADS = %w(SCHEMA EXPLAIN) EXPLAINED_SQLS = /\A\s*(with|select|update|delete|insert)\b/i def ignore_payload?(payload) - payload[:exception] || IGNORED_PAYLOADS.include?(payload[:name]) || payload[:sql] !~ EXPLAINED_SQLS + payload[:exception] || + payload[:cached] || + IGNORED_PAYLOADS.include?(payload[:name]) || + payload[:sql] !~ EXPLAINED_SQLS end ActiveSupport::Notifications.subscribe("sql.active_record", new) diff --git a/activerecord/lib/active_record/fixture_set/file.rb b/activerecord/lib/active_record/fixture_set/file.rb index 8132310c91..6cf2e01179 100644 --- a/activerecord/lib/active_record/fixture_set/file.rb +++ b/activerecord/lib/active_record/fixture_set/file.rb @@ -1,5 +1,5 @@ -require 'erb' -require 'yaml' +require "erb" +require "yaml" module ActiveRecord class FixtureSet @@ -17,38 +17,62 @@ module ActiveRecord def initialize(file) @file = file - @rows = nil end def each(&block) rows.each(&block) end + def model_class + config_row["model_class"] + end private def rows - return @rows if @rows + @rows ||= raw_rows.reject { |fixture_name, _| fixture_name == "_fixture" } + end + + def config_row + @config_row ||= begin + row = raw_rows.find { |fixture_name, _| fixture_name == "_fixture" } + if row + row.last + else + { 'model_class': nil } + end + end + end - begin + def raw_rows + @raw_rows ||= begin data = YAML.load(render(IO.read(@file))) + data ? validate(data).to_a : [] rescue ArgumentError, Psych::SyntaxError => error raise Fixture::FormatError, "a YAML error occurred parsing #{@file}. Please note that YAML must be consistently indented using spaces. Tabs are not allowed. Please have a look at http://www.yaml.org/faq.html\nThe exact error was:\n #{error.class}: #{error}", error.backtrace end - @rows = data ? validate(data).to_a : [] + end + + def prepare_erb(content) + erb = ERB.new(content) + erb.filename = @file + erb end def render(content) context = ActiveRecord::FixtureSet::RenderContext.create_subclass.new - ERB.new(content).result(context.get_binding) + prepare_erb(content).result(context.get_binding) end # Validate our unmarshalled data. def validate(data) unless Hash === data || YAML::Omap === data - raise Fixture::FormatError, 'fixture is not a hash' + raise Fixture::FormatError, "fixture is not a hash: #{@file}" end - raise Fixture::FormatError unless data.all? { |name, row| Hash === row } + invalid = data.reject { |_, row| Hash === row } + if invalid.any? + raise Fixture::FormatError, "fixture key is not a hash: #{@file}, keys: #{invalid.keys.inspect}" + end data end end diff --git a/activerecord/lib/active_record/fixtures.rb b/activerecord/lib/active_record/fixtures.rb index b01444a090..de1b0d63bc 100644 --- a/activerecord/lib/active_record/fixtures.rb +++ b/activerecord/lib/active_record/fixtures.rb @@ -1,11 +1,11 @@ -require 'erb' -require 'yaml' -require 'zlib' -require 'set' -require 'active_support/dependencies' -require 'active_support/core_ext/digest/uuid' -require 'active_record/fixture_set/file' -require 'active_record/errors' +require "erb" +require "yaml" +require "zlib" +require "set" +require "active_support/dependencies" +require "active_support/core_ext/digest/uuid" +require "active_record/fixture_set/file" +require "active_record/errors" module ActiveRecord class FixtureClassNotFound < ActiveRecord::ActiveRecordError #:nodoc: @@ -66,7 +66,7 @@ module ActiveRecord # By default, +test_helper.rb+ will load all of your fixtures into your test # database, so this test will succeed. # - # The testing environment will automatically load the all fixtures into the database before each + # The testing environment will automatically load all the fixtures into the database before each # test. To ensure consistent data, the environment deletes the fixtures before running the load. # # In addition to being available in the database, the fixture's data may also be accessed by @@ -88,8 +88,8 @@ module ActiveRecord # assert_equal "Ruby on Rails", @rubyonrails.name # end # - # In order to use these methods to access fixtured data within your testcases, you must specify one of the - # following in your <tt>ActiveSupport::TestCase</tt>-derived class: + # In order to use these methods to access fixtured data within your test cases, you must specify one of the + # following in your ActiveSupport::TestCase-derived class: # # - to fully enable instantiated fixtures (enable alternate methods #1 and #2 above) # self.use_instantiated_fixtures = true @@ -103,7 +103,7 @@ module ActiveRecord # # = Dynamic fixtures with ERB # - # Some times you don't care about the content of the fixtures as much as you care about the volume. + # Sometimes you don't care about the content of the fixtures as much as you care about the volume. # In these cases, you can mix ERB in with your YAML fixtures to create a bunch of fixtures for load # testing, like: # @@ -124,7 +124,7 @@ module ActiveRecord # # Helper methods defined in a fixture will not be available in other fixtures, to prevent against # unwanted inter-test dependencies. Methods used by multiple fixtures should be defined in a module - # that is included in <tt>ActiveRecord::FixtureSet.context_class</tt>. + # that is included in ActiveRecord::FixtureSet.context_class. # # - define a helper method in `test_helper.rb` # module FixtureFileHelpers @@ -395,15 +395,29 @@ module ActiveRecord # <<: *DEFAULTS # # Any fixture labeled "DEFAULTS" is safely ignored. + # + # == Configure the fixture model class + # + # It's possible to set the fixture's model class directly in the YAML file. + # This is helpful when fixtures are loaded outside tests and + # +set_fixture_class+ is not available (e.g. + # when running <tt>rails db:fixtures:load</tt>). + # + # _fixture: + # model_class: User + # david: + # name: David + # + # Any fixtures labeled "_fixture" are safely ignored. class FixtureSet #-- # An instance of FixtureSet is normally stored in a single YAML file and # possibly in a folder with the same name. #++ - MAX_ID = 2 ** 30 - 1 + MAX_ID = 2**30 - 1 - @@all_cached_fixtures = Hash.new { |h,k| h[k] = {} } + @@all_cached_fixtures = Hash.new { |h, k| h[k] = {} } def self.default_fixture_model_name(fixture_set_name, config = ActiveRecord::Base) # :nodoc: config.pluralize_table_names ? @@ -412,9 +426,9 @@ module ActiveRecord end def self.default_fixture_table_name(fixture_set_name, config = ActiveRecord::Base) # :nodoc: - "#{ config.table_name_prefix }"\ - "#{ fixture_set_name.tr('/', '_') }"\ - "#{ config.table_name_suffix }".to_sym + "#{ config.table_name_prefix }"\ + "#{ fixture_set_name.tr('/', '_') }"\ + "#{ config.table_name_suffix }".to_sym end def self.reset_cache @@ -480,18 +494,18 @@ module ActiveRecord private - def insert_class(class_names, name, klass) - # We only want to deal with AR objects. - if klass && klass < ActiveRecord::Base - class_names[name] = klass - else - class_names[name] = nil + def insert_class(class_names, name, klass) + # We only want to deal with AR objects. + if klass && klass < ActiveRecord::Base + class_names[name] = klass + else + class_names[name] = nil + end end - end - def default_fixture_model(fs_name, config) - ActiveRecord::FixtureSet.default_fixture_model_name(fs_name, config) - end + def default_fixture_model(fs_name, config) + ActiveRecord::FixtureSet.default_fixture_model_name(fs_name, config) + end end def self.create_fixtures(fixtures_directory, fixture_set_names, class_names = {}, config = ActiveRecord::Base) @@ -521,17 +535,17 @@ module ActiveRecord update_all_loaded_fixtures fixtures_map - connection.transaction(:requires_new => true) do - deleted_tables = Set.new + connection.transaction(requires_new: true) do + deleted_tables = Hash.new { |h, k| h[k] = Set.new } fixture_sets.each do |fs| conn = fs.model_class.respond_to?(:connection) ? fs.model_class.connection : connection table_rows = fs.table_rows table_rows.each_key do |table| - unless deleted_tables.include? table - conn.delete "DELETE FROM #{conn.quote_table_name(table)}", 'Fixture Delete' + unless deleted_tables[conn].include? table + conn.delete "DELETE FROM #{conn.quote_table_name(table)}", "Fixture Delete" end - deleted_tables << table + deleted_tables[conn] << table end table_rows.each do |fixture_set_name, rows| @@ -578,28 +592,23 @@ module ActiveRecord @name = name @path = path @config = config - @model_class = nil - if class_name.is_a?(Class) # TODO: Should be an AR::Base type class, or any? - @model_class = class_name - else - @model_class = class_name.safe_constantize if class_name - end + self.model_class = class_name - @connection = connection + @fixtures = read_fixture_files(path) - @table_name = ( model_class.respond_to?(:table_name) ? - model_class.table_name : - self.class.default_fixture_table_name(name, config) ) + @connection = connection - @fixtures = read_fixture_files path, @model_class + @table_name = (model_class.respond_to?(:table_name) ? + model_class.table_name : + self.class.default_fixture_table_name(name, config)) end def [](x) fixtures[x] end - def []=(k,v) + def []=(k, v) fixtures[k] = v end @@ -617,10 +626,10 @@ module ActiveRecord now = config.default_timezone == :utc ? Time.now.utc : Time.now # allow a standard key to be used for doing defaults in YAML - fixtures.delete('DEFAULTS') + fixtures.delete("DEFAULTS") # track any join tables we need to insert later - rows = Hash.new { |h,table| h[table] = [] } + rows = Hash.new { |h, table| h[table] = [] } rows[table_name] = fixtures.map do |label, fixture| row = fixture.to_hash @@ -645,7 +654,9 @@ module ActiveRecord # Resolve enums model_class.defined_enums.each do |name, values| - row[name] = values.fetch(row[name], row[name]) + if row.include?(name) + row[name] = values.fetch(row[name], row[name]) + end end # If STI is used, find the correct subclass for association reflection @@ -759,13 +770,25 @@ module ActiveRecord @column_names ||= @connection.columns(@table_name).collect(&:name) end - def read_fixture_files(path, model_class) + def model_class=(class_name) + if class_name.is_a?(Class) # TODO: Should be an AR::Base type class, or any? + @model_class = class_name + else + @model_class = class_name.safe_constantize if class_name + end + end + + # Loads the fixtures from the YAML file at +path+. + # If the file sets the +model_class+ and current instance value is not set, + # it uses the file value. + def read_fixture_files(path) yaml_files = Dir["#{path}/{**,*}/*.yml"].select { |f| ::File.file?(f) } + [yaml_file_path(path)] yaml_files.each_with_object({}) do |file, fixtures| FixtureSet::File.open(file) do |fh| + self.model_class ||= fh.model_class if fh.model_class fh.each do |fixture_name, row| fixtures[fixture_name] = ActiveRecord::Fixture.new(row, model_class) end @@ -776,7 +799,6 @@ module ActiveRecord def yaml_file_path(path) "#{path}.yml" end - end class Fixture #:nodoc: @@ -825,46 +847,32 @@ module ActiveRecord module TestFixtures extend ActiveSupport::Concern - def before_setup + def before_setup # :nodoc: setup_fixtures super end - def after_teardown + def after_teardown # :nodoc: super teardown_fixtures end included do - class_attribute :fixture_path, :instance_writer => false + class_attribute :fixture_path, instance_writer: false class_attribute :fixture_table_names class_attribute :fixture_class_names class_attribute :use_transactional_tests - class_attribute :use_transactional_fixtures class_attribute :use_instantiated_fixtures # true, false, or :no_instances class_attribute :pre_loaded_fixtures class_attribute :config - singleton_class.deprecate 'use_transactional_fixtures=' => 'use use_transactional_tests= instead' - self.fixture_table_names = [] self.use_instantiated_fixtures = false self.pre_loaded_fixtures = false self.config = ActiveRecord::Base - self.fixture_class_names = Hash.new do |h, fixture_set_name| - h[fixture_set_name] = ActiveRecord::FixtureSet.default_fixture_model_name(fixture_set_name, self.config) - end - - silence_warnings do - define_singleton_method :use_transactional_tests do - if use_transactional_fixtures.nil? - true - else - use_transactional_fixtures - end - end - end + self.fixture_class_names = {} + self.use_transactional_tests = true end module ClassMethods @@ -882,7 +890,7 @@ module ActiveRecord def fixtures(*fixture_set_names) if fixture_set_names.first == :all - fixture_set_names = Dir["#{fixture_path}/{**,*}/*.{yml}"] + fixture_set_names = Dir["#{fixture_path}/{**,*}/*.{yml}"].uniq fixture_set_names.map! { |f| f[(fixture_path.to_s.size + 1)..-5] } else fixture_set_names = fixture_set_names.flatten.map(&:to_s) @@ -897,7 +905,7 @@ module ActiveRecord methods = Module.new do fixture_set_names.each do |fs_name| fs_name = fs_name.to_s - accessor_name = fs_name.tr('/', '_').to_sym + accessor_name = fs_name.tr("/", "_").to_sym define_method(accessor_name) do |*fixture_names| force_reload = fixture_names.pop if fixture_names.last == true || fixture_names.last == :reload @@ -941,12 +949,13 @@ module ActiveRecord def setup_fixtures(config = ActiveRecord::Base) if pre_loaded_fixtures && !use_transactional_tests - raise RuntimeError, 'pre_loaded_fixtures requires use_transactional_tests' + raise RuntimeError, "pre_loaded_fixtures requires use_transactional_tests" end @fixture_cache = {} @fixture_connections = [] @@already_loaded_fixtures ||= {} + @connection_subscriber = nil # Load fixtures once and begin transaction. if run_in_transaction? @@ -956,10 +965,31 @@ module ActiveRecord @loaded_fixtures = load_fixtures(config) @@already_loaded_fixtures[self.class] = @loaded_fixtures end + + # Begin transactions for connections already established @fixture_connections = enlist_fixture_connections @fixture_connections.each do |connection| connection.begin_transaction joinable: false end + + # When connections are established in the future, begin a transaction too + @connection_subscriber = ActiveSupport::Notifications.subscribe("!connection.active_record") do |_, _, _, _, payload| + spec_name = payload[:spec_name] if payload.key?(:spec_name) + + if spec_name + begin + connection = ActiveRecord::Base.connection_handler.retrieve_connection(spec_name) + rescue ConnectionNotEstablished + connection = nil + end + + if connection && !@fixture_connections.include?(connection) + connection.begin_transaction joinable: false + @fixture_connections << connection + end + end + end + # Load fixtures for every test. else ActiveRecord::FixtureSet.reset_cache @@ -974,6 +1004,7 @@ module ActiveRecord def teardown_fixtures # Rollback changes if a transaction is active. if run_in_transaction? + ActiveSupport::Notifications.unsubscribe(@connection_subscriber) if @connection_subscriber @fixture_connections.each do |connection| connection.rollback_transaction if connection.transaction_open? end @@ -997,10 +1028,10 @@ module ActiveRecord def instantiate_fixtures if pre_loaded_fixtures - raise RuntimeError, 'Load fixtures before instantiating them.' if ActiveRecord::FixtureSet.all_loaded_fixtures.empty? + raise RuntimeError, "Load fixtures before instantiating them." if ActiveRecord::FixtureSet.all_loaded_fixtures.empty? ActiveRecord::FixtureSet.instantiate_all_loaded_fixtures(self, load_instances?) else - raise RuntimeError, 'Load fixtures before instantiating them.' if @loaded_fixtures.nil? + raise RuntimeError, "Load fixtures before instantiating them." if @loaded_fixtures.nil? @loaded_fixtures.each_value do |fixture_set| ActiveRecord::FixtureSet.instantiate_fixtures(self, fixture_set, load_instances?) end diff --git a/activerecord/lib/active_record/gem_version.rb b/activerecord/lib/active_record/gem_version.rb index a388b529c9..f33456a744 100644 --- a/activerecord/lib/active_record/gem_version.rb +++ b/activerecord/lib/active_record/gem_version.rb @@ -6,7 +6,7 @@ module ActiveRecord module VERSION MAJOR = 5 - MINOR = 0 + MINOR = 1 TINY = 0 PRE = "alpha" diff --git a/activerecord/lib/active_record/inheritance.rb b/activerecord/lib/active_record/inheritance.rb index e613d157aa..a1d4f47372 100644 --- a/activerecord/lib/active_record/inheritance.rb +++ b/activerecord/lib/active_record/inheritance.rb @@ -1,4 +1,4 @@ -require 'active_support/core_ext/hash/indifferent_access' +require "active_support/core_ext/hash/indifferent_access" module ActiveRecord # == Single table inheritance @@ -19,7 +19,7 @@ module ActiveRecord # Be aware that because the type column is an attribute on the record every new # subclass will instantly be marked as dirty and the type column will be included # in the list of changed attributes on the record. This is different from non - # STI classes: + # Single Table Inheritance(STI) classes: # # Company.new.changed? # => false # Firm.new.changed? # => true @@ -37,6 +37,7 @@ module ActiveRecord included do # Determines whether to store the full constant name including namespace when using STI. + # This is true, by default. class_attribute :store_full_sti_class, instance_writer: false self.store_full_sti_class = true end @@ -51,11 +52,15 @@ module ActiveRecord end attrs = args.first - if subclass_from_attributes?(attrs) + if has_attribute?(inheritance_column) subclass = subclass_from_attributes(attrs) + + if subclass.nil? && base_class == self + subclass = subclass_from_attributes(column_defaults) + end end - if subclass + if subclass && subclass != self subclass.new(*args, &block) else super @@ -82,7 +87,7 @@ module ActiveRecord # 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 + # If A extends ActiveRecord::Base, A.base_class will return A. If B descends from A # through some arbitrarily deep hierarchy, B.base_class will return A. # # If B < A and C < B and if A is an abstract_class then both B.base_class @@ -127,89 +132,85 @@ module ActiveRecord protected - # Returns the class type of the record using the current module as a prefix. So descendants of - # MyApp::Business::Account would appear as MyApp::Business::AccountSubclass. - def compute_type(type_name) - if type_name.match(/^::/) - # If the type is prefixed with a scope operator then we assume that - # the type_name is an absolute reference. - ActiveSupport::Dependencies.constantize(type_name) - else - # Build a list of candidates to search for - candidates = [] - name.scan(/::|$/) { candidates.unshift "#{$`}::#{type_name}" } - candidates << type_name - - candidates.each do |candidate| - constant = ActiveSupport::Dependencies.safe_constantize(candidate) - return constant if candidate == constant.to_s - end + # Returns the class type of the record using the current module as a prefix. So descendants of + # MyApp::Business::Account would appear as MyApp::Business::AccountSubclass. + def compute_type(type_name) + if type_name.match(/^::/) + # If the type is prefixed with a scope operator then we assume that + # the type_name is an absolute reference. + ActiveSupport::Dependencies.constantize(type_name) + else + # Build a list of candidates to search for + candidates = [] + name.scan(/::|$/) { candidates.unshift "#{$`}::#{type_name}" } + candidates << type_name + + candidates.each do |candidate| + constant = ActiveSupport::Dependencies.safe_constantize(candidate) + return constant if candidate == constant.to_s + end - raise NameError.new("uninitialized constant #{candidates.first}", candidates.first) + raise NameError.new("uninitialized constant #{candidates.first}", candidates.first) + end end - end private - # Called by +instantiate+ to decide which class to use for a new - # record instance. For single-table inheritance, we check the record - # for a +type+ column and return the corresponding class. - def discriminate_class_for_record(record) - if using_single_table_inheritance?(record) - find_sti_class(record[inheritance_column]) - else - super + # Called by +instantiate+ to decide which class to use for a new + # record instance. For single-table inheritance, we check the record + # for a +type+ column and return the corresponding class. + def discriminate_class_for_record(record) + if using_single_table_inheritance?(record) + find_sti_class(record[inheritance_column]) + else + super + end end - end - - def using_single_table_inheritance?(record) - record[inheritance_column].present? && columns_hash.include?(inheritance_column) - end - def find_sti_class(type_name) - if store_full_sti_class - ActiveSupport::Dependencies.constantize(type_name) - else - compute_type(type_name) + def using_single_table_inheritance?(record) + record[inheritance_column].present? && has_attribute?(inheritance_column) end - rescue NameError - raise SubclassNotFound, - "The single-table inheritance mechanism failed to locate the subclass: '#{type_name}'. " + - "This error is raised because the column '#{inheritance_column}' is reserved for storing the class in case of inheritance. " + - "Please rename this column if you didn't intend it to be used for storing the inheritance class " + - "or overwrite #{name}.inheritance_column to use another column for that information." - end - - def type_condition(table = arel_table) - sti_column = table[inheritance_column] - sti_names = ([self] + descendants).map(&:sti_name) - sti_column.in(sti_names) - end + def find_sti_class(type_name) + type_name = base_class.type_for_attribute(inheritance_column).cast(type_name) + subclass = begin + if store_full_sti_class + ActiveSupport::Dependencies.constantize(type_name) + else + compute_type(type_name) + end + rescue NameError + raise SubclassNotFound, + "The single-table inheritance mechanism failed to locate the subclass: '#{type_name}'. " \ + "This error is raised because the column '#{inheritance_column}' is reserved for storing the class in case of inheritance. " \ + "Please rename this column if you didn't intend it to be used for storing the inheritance class " \ + "or overwrite #{name}.inheritance_column to use another column for that information." + end + unless subclass == self || descendants.include?(subclass) + raise SubclassNotFound, "Invalid single-table inheritance type: #{subclass.name} is not a subclass of #{name}" + end + subclass + end - # Detect the subclass from the inheritance column of attrs. If the inheritance column value - # is not self or a valid subclass, raises ActiveRecord::SubclassNotFound - # If this is a StrongParameters hash, and access to inheritance_column is not permitted, - # this will ignore the inheritance column and return nil - def subclass_from_attributes?(attrs) - attribute_names.include?(inheritance_column) && attrs.is_a?(Hash) - end + def type_condition(table = arel_table) + sti_column = arel_attribute(inheritance_column, table) + sti_names = ([self] + descendants).map(&:sti_name) - def subclass_from_attributes(attrs) - subclass_name = attrs.with_indifferent_access[inheritance_column] + sti_column.in(sti_names) + end - if subclass_name.present? - subclass = find_sti_class(subclass_name) + # Detect the subclass from the inheritance column of attrs. If the inheritance column value + # is not self or a valid subclass, raises ActiveRecord::SubclassNotFound + def subclass_from_attributes(attrs) + attrs = attrs.to_h if attrs.respond_to?(:permitted?) + if attrs.is_a?(Hash) + subclass_name = attrs.with_indifferent_access[inheritance_column] - if subclass.name != self.name - unless descendants.include?(subclass) - raise ActiveRecord::SubclassNotFound.new("Invalid single-table inheritance type: #{subclass.name} is not a subclass of #{name}") + if subclass_name.present? + find_sti_class(subclass_name) end - - subclass end end - end end def initialize_dup(other) @@ -219,21 +220,21 @@ module ActiveRecord private - def initialize_internals_callback - super - ensure_proper_type - end + def initialize_internals_callback + super + ensure_proper_type + end - # Sets the attribute used for single table inheritance to this class name if this is not the - # ActiveRecord::Base descendant. - # Considering the hierarchy Reply < Message < ActiveRecord::Base, this makes it possible to - # do Reply.new without having to set <tt>Reply[Reply.inheritance_column] = "Reply"</tt> yourself. - # No such attribute would be set for objects of the Message class in that example. - def ensure_proper_type - klass = self.class - if klass.finder_needs_type_condition? - write_attribute(klass.inheritance_column, klass.sti_name) + # Sets the attribute used for single table inheritance to this class name if this is not the + # ActiveRecord::Base descendant. + # Considering the hierarchy Reply < Message < ActiveRecord::Base, this makes it possible to + # do Reply.new without having to set <tt>Reply[Reply.inheritance_column] = "Reply"</tt> yourself. + # No such attribute would be set for objects of the Message class in that example. + def ensure_proper_type + klass = self.class + if klass.finder_needs_type_condition? + write_attribute(klass.inheritance_column, klass.sti_name) + end end - end end end diff --git a/activerecord/lib/active_record/integration.rb b/activerecord/lib/active_record/integration.rb index 15b2f65dcb..8e71b60b29 100644 --- a/activerecord/lib/active_record/integration.rb +++ b/activerecord/lib/active_record/integration.rb @@ -1,4 +1,4 @@ -require 'active_support/core_ext/string/filters' +require "active_support/core_ext/string/filters" module ActiveRecord module Integration @@ -10,14 +10,14 @@ module ActiveRecord # Indicates the format used to generate the timestamp in the cache key. # Accepts any of the symbols in <tt>Time::DATE_FORMATS</tt>. # - # This is +:nsec+, by default. - class_attribute :cache_timestamp_format, :instance_writer => false - self.cache_timestamp_format = :nsec + # This is +:usec+, by default. + class_attribute :cache_timestamp_format, instance_writer: false + self.cache_timestamp_format = :usec end - # Returns a String, which Action Pack uses for constructing an URL to this - # object. The default implementation returns this record's id as a String, - # or nil if this record's unsaved. + # Returns a +String+, which Action Pack uses for constructing a URL to this + # object. The default implementation returns this record's id as a +String+, + # or +nil+ if this record's unsaved. # # For example, suppose that you have a User model, and that you have a # <tt>resources :users</tt> route. Normally, +user_path+ will @@ -53,18 +53,21 @@ module ActiveRecord # # Person.find(5).cache_key(:updated_at, :last_reviewed_at) def cache_key(*timestamp_names) - case - when new_record? + if new_record? "#{model_name.cache_key}/new" - when timestamp_names.any? - timestamp = max_updated_column_timestamp(timestamp_names) - timestamp = timestamp.utc.to_s(cache_timestamp_format) - "#{model_name.cache_key}/#{id}-#{timestamp}" - when timestamp = max_updated_column_timestamp - timestamp = timestamp.utc.to_s(cache_timestamp_format) - "#{model_name.cache_key}/#{id}-#{timestamp}" else - "#{model_name.cache_key}/#{id}" + timestamp = if timestamp_names.any? + max_updated_column_timestamp(timestamp_names) + else + max_updated_column_timestamp + end + + if timestamp + timestamp = timestamp.utc.to_s(cache_timestamp_format) + "#{model_name.cache_key}/#{id}-#{timestamp}" + else + "#{model_name.cache_key}/#{id}" + end end end @@ -84,9 +87,9 @@ module ActiveRecord # Values longer than 20 characters will be truncated. The value # is truncated word by word. # - # user = User.find_by(name: 'David HeinemeierHansson') + # user = User.find_by(name: 'David Heinemeier Hansson') # user.id # => 125 - # user_path(user) # => "/users/125-david" + # user_path(user) # => "/users/125-david-heinemeier" # # Because the generated param begins with the record's +id+, it is # suitable for passing to +find+. In a controller, for example: @@ -100,7 +103,7 @@ module ActiveRecord define_method :to_param do if (default = super()) && (result = send(method_name).to_s).present? && - (param = result.squish.truncate(20, separator: /\s/, omission: nil).parameterize).present? + (param = result.squish.parameterize.truncate(20, separator: /-/, omission: "")).present? "#{default}-#{param}" else default diff --git a/activerecord/lib/active_record/internal_metadata.rb b/activerecord/lib/active_record/internal_metadata.rb new file mode 100644 index 0000000000..25ee9d6bfe --- /dev/null +++ b/activerecord/lib/active_record/internal_metadata.rb @@ -0,0 +1,43 @@ +require "active_record/scoping/default" +require "active_record/scoping/named" + +module ActiveRecord + # This class is used to create a table that keeps track of values and keys such + # as which environment migrations were run in. + class InternalMetadata < ActiveRecord::Base # :nodoc: + class << self + def primary_key + "key" + end + + def table_name + "#{table_name_prefix}#{ActiveRecord::Base.internal_metadata_table_name}#{table_name_suffix}" + end + + def []=(key, value) + find_or_initialize_by(key: key).update_attributes!(value: value) + end + + def [](key) + where(key: key).pluck(:value).first + end + + def table_exists? + connection.table_exists?(table_name) + end + + # Creates an internal metadata table with columns +key+ and +value+ + def create_table + unless table_exists? + key_options = connection.internal_string_options_for_primary_key + + connection.create_table(table_name, id: false) do |t| + t.string :key, key_options + t.string :value + t.timestamps + end + end + end + end + end +end diff --git a/activerecord/lib/active_record/legacy_yaml_adapter.rb b/activerecord/lib/active_record/legacy_yaml_adapter.rb index 89dee58423..c7683f68c7 100644 --- a/activerecord/lib/active_record/legacy_yaml_adapter.rb +++ b/activerecord/lib/active_record/legacy_yaml_adapter.rb @@ -4,7 +4,7 @@ module ActiveRecord return coder unless coder.is_a?(Psych::Coder) case coder["active_record_yaml_version"] - when 1 then coder + when 1, 2 then coder else if coder["attributes"].is_a?(AttributeSet) Rails420.convert(klass, coder) diff --git a/activerecord/lib/active_record/locale/en.yml b/activerecord/lib/active_record/locale/en.yml index 8a3c27e6da..0b35027b2b 100644 --- a/activerecord/lib/active_record/locale/en.yml +++ b/activerecord/lib/active_record/locale/en.yml @@ -16,8 +16,8 @@ en: messages: record_invalid: "Validation failed: %{errors}" restrict_dependent_destroy: - one: "Cannot delete record because a dependent %{record} exists" - many: "Cannot delete record because dependent %{record} exist" + has_one: "Cannot delete record because a dependent %{record} exists" + has_many: "Cannot delete record because dependent %{record} exist" # Append your own errors here or at the model/attributes scope. # You can define own errors for models or model attributes. diff --git a/activerecord/lib/active_record/locking/optimistic.rb b/activerecord/lib/active_record/locking/optimistic.rb index a09437b4b0..2659c60f1f 100644 --- a/activerecord/lib/active_record/locking/optimistic.rb +++ b/activerecord/lib/active_record/locking/optimistic.rb @@ -22,7 +22,7 @@ module ActiveRecord # p1.save # # p2.first_name = "should fail" - # p2.save # Raises a ActiveRecord::StaleObjectError + # p2.save # Raises an ActiveRecord::StaleObjectError # # Optimistic locking will also check for stale data when objects are destroyed. Example: # @@ -32,7 +32,7 @@ module ActiveRecord # p1.first_name = "Michael" # p1.save # - # p2.destroy # Raises a ActiveRecord::StaleObjectError + # p2.destroy # Raises an ActiveRecord::StaleObjectError # # You're then responsible for dealing with the conflict by rescuing the exception and either rolling back, merging, # or otherwise apply the business logic needed to resolve the conflict. @@ -47,6 +47,8 @@ module ActiveRecord # self.locking_column = :lock_person # end # + # Please note that the optimistic locking will be ignored if you update the + # locking column's value. module Optimistic extend ActiveSupport::Concern @@ -60,13 +62,14 @@ module ActiveRecord end private + def increment_lock lock_col = self.class.locking_column previous_lock_value = send(lock_col).to_i - send(lock_col + '=', previous_lock_value + 1) + send(lock_col + "=", previous_lock_value + 1) end - def _create_record(attribute_names = self.attribute_names, *) # :nodoc: + def _create_record(attribute_names = self.attribute_names, *) if locking_enabled? # We always want to persist the locking version, even if we don't detect # a change from the default, since the database might have no default @@ -75,23 +78,26 @@ module ActiveRecord super end - def _update_record(attribute_names = self.attribute_names) #:nodoc: + def _update_record(attribute_names = self.attribute_names) return super unless locking_enabled? - return 0 if attribute_names.empty? lock_col = self.class.locking_column - previous_lock_value = send(lock_col).to_i - increment_lock - attribute_names += [lock_col] - attribute_names.uniq! + return super if attribute_names.include?(lock_col) + return 0 if attribute_names.empty? begin + previous_lock_value = read_attribute_before_type_cast(lock_col) + + increment_lock + + attribute_names.push(lock_col) + relation = self.class.unscoped affected_rows = relation.where( self.class.primary_key => id, - lock_col => previous_lock_value, + lock_col => previous_lock_value ).update_all( attributes_for_update(attribute_names).map do |name| [name, _read_attribute(name)] @@ -104,9 +110,9 @@ module ActiveRecord affected_rows - # If something went wrong, revert the version. + # If something went wrong, revert the locking_column value. rescue Exception - send(lock_col + '=', previous_lock_value) + send(lock_col + "=", previous_lock_value.to_i) raise end end @@ -132,70 +138,76 @@ module ActiveRecord relation end - module ClassMethods - DEFAULT_LOCKING_COLUMN = 'lock_version' + module ClassMethods + DEFAULT_LOCKING_COLUMN = "lock_version" - # Returns true if the +lock_optimistically+ flag is set to true - # (which it is, by default) and the table includes the - # +locking_column+ column (defaults to +lock_version+). - def locking_enabled? - lock_optimistically && columns_hash[locking_column] - end + # Returns true if the +lock_optimistically+ flag is set to true + # (which it is, by default) and the table includes the + # +locking_column+ column (defaults to +lock_version+). + def locking_enabled? + lock_optimistically && columns_hash[locking_column] + end - # Set the column to use for optimistic locking. Defaults to +lock_version+. - def locking_column=(value) - reload_schema_from_cache - @locking_column = value.to_s - end + # Set the column to use for optimistic locking. Defaults to +lock_version+. + def locking_column=(value) + reload_schema_from_cache + @locking_column = value.to_s + end - # The version column used for optimistic locking. Defaults to +lock_version+. - def locking_column - reset_locking_column unless defined?(@locking_column) - @locking_column - end + # The version column used for optimistic locking. Defaults to +lock_version+. + def locking_column + @locking_column = DEFAULT_LOCKING_COLUMN unless defined?(@locking_column) + @locking_column + end - # Reset the column used for optimistic locking back to the +lock_version+ default. - def reset_locking_column - self.locking_column = DEFAULT_LOCKING_COLUMN - end + # Reset the column used for optimistic locking back to the +lock_version+ default. + def reset_locking_column + self.locking_column = DEFAULT_LOCKING_COLUMN + end - # Make sure the lock version column gets updated when counters are - # updated. - def update_counters(id, counters) - counters = counters.merge(locking_column => 1) if locking_enabled? - super - end + # Make sure the lock version column gets updated when counters are + # updated. + def update_counters(id, counters) + counters = counters.merge(locking_column => 1) if locking_enabled? + super + end - private - - # We need to apply this decorator here, rather than on module inclusion. The closure - # created by the matcher would otherwise evaluate for `ActiveRecord::Base`, not the - # sub class being decorated. As such, changes to `lock_optimistically`, or - # `locking_column` would not be picked up. - def inherited(subclass) - subclass.class_eval do - is_lock_column = ->(name, _) { lock_optimistically && name == locking_column } - decorate_matching_attribute_types(is_lock_column, :_optimistic_locking) do |type| - LockingType.new(type) + private + + # We need to apply this decorator here, rather than on module inclusion. The closure + # created by the matcher would otherwise evaluate for `ActiveRecord::Base`, not the + # sub class being decorated. As such, changes to `lock_optimistically`, or + # `locking_column` would not be picked up. + def inherited(subclass) + subclass.class_eval do + is_lock_column = ->(name, _) { lock_optimistically && name == locking_column } + decorate_matching_attribute_types(is_lock_column, :_optimistic_locking) do |type| + LockingType.new(type) + end + end + super end - end - super end - end end + # In de/serialize we change `nil` to 0, so that we can allow passing + # `nil` values to `lock_version`, and not result in `ActiveRecord::StaleObjectError` + # during update record. class LockingType < DelegateClass(Type::Value) # :nodoc: def deserialize(value) - # `nil` *should* be changed to 0 + super.to_i + end + + def serialize(value) super.to_i end def init_with(coder) - __setobj__(coder['subtype']) + __setobj__(coder["subtype"]) end def encode_with(coder) - coder['subtype'] = __getobj__ + coder["subtype"] = __getobj__ end end end diff --git a/activerecord/lib/active_record/locking/pessimistic.rb b/activerecord/lib/active_record/locking/pessimistic.rb index 3d95c54ef3..e73cb4fc12 100644 --- a/activerecord/lib/active_record/locking/pessimistic.rb +++ b/activerecord/lib/active_record/locking/pessimistic.rb @@ -51,7 +51,7 @@ module ActiveRecord # end # # Database-specific information on row locking: - # MySQL: http://dev.mysql.com/doc/refman/5.6/en/innodb-locking-reads.html + # MySQL: http://dev.mysql.com/doc/refman/5.7/en/innodb-locking-reads.html # PostgreSQL: http://www.postgresql.org/docs/current/interactive/sql-select.html#SQL-FOR-UPDATE-SHARE module Pessimistic # Obtain a row lock on this record. Reloads the record to obtain the requested @@ -59,7 +59,7 @@ module ActiveRecord # or pass true for "FOR UPDATE" (the default, an exclusive row lock). Returns # the locked record. def lock!(lock = true) - reload(:lock => lock) if persisted? + reload(lock: lock) if persisted? self end diff --git a/activerecord/lib/active_record/log_subscriber.rb b/activerecord/lib/active_record/log_subscriber.rb index 4d597a0ab1..4b8d8d9105 100644 --- a/activerecord/lib/active_record/log_subscriber.rb +++ b/activerecord/lib/active_record/log_subscriber.rb @@ -15,58 +15,84 @@ module ActiveRecord rt end - def initialize - super - @odd = false - end - - def render_bind(attribute) - value = if attribute.type.binary? && attribute.value - "<#{attribute.value.bytesize} bytes of binary data>" - else - attribute.value_for_database - end - - [attribute.name, value] - end - def sql(event) - return unless logger.debug? - self.class.runtime += event.duration + return unless logger.debug? payload = event.payload return if IGNORE_PAYLOAD_NAMES.include?(payload[:name]) name = "#{payload[:name]} (#{event.duration.round(1)}ms)" + name = "CACHE #{name}" if payload[:cached] sql = payload[:sql] binds = nil unless (payload[:binds] || []).empty? - binds = " " + payload[:binds].map { |attr| render_bind(attr) }.inspect + casted_params = type_casted_binds(payload[:binds], payload[:type_casted_binds]) + binds = " " + payload[:binds].zip(casted_params).map { |attr, value| + render_bind(attr, value) + }.inspect end - name = color(name, nil, true) + name = colorize_payload_name(name, payload[:name]) sql = color(sql, sql_color(sql), true) debug " #{name} #{sql}#{binds}" end - def sql_color(sql) - case sql - when /\s*\Ainsert/i then GREEN - when /\s*\Aselect/i then BLUE - when /\s*\Aupdate/i then YELLOW - when /\s*\Adelete/i then RED - when /transaction\s*\Z/i then CYAN - else MAGENTA + private + + def type_casted_binds(binds, casted_binds) + casted_binds || binds.map { |attr| type_cast attr.value_for_database } end - end - def logger - ActiveRecord::Base.logger - end + def render_bind(attr, type_casted_value) + value = if attr.type.binary? && attr.value + "<#{attr.value_for_database.to_s.bytesize} bytes of binary data>" + else + type_casted_value + end + + [attr.name, value] + end + + def colorize_payload_name(name, payload_name) + if payload_name.blank? || payload_name == "SQL" # SQL vs Model Load/Exists + color(name, MAGENTA, true) + else + color(name, CYAN, true) + end + end + + def sql_color(sql) + case sql + when /\A\s*rollback/mi + RED + when /select .*for update/mi, /\A\s*lock/mi + WHITE + when /\A\s*select/i + BLUE + when /\A\s*insert/i + GREEN + when /\A\s*update/i + YELLOW + when /\A\s*delete/i + RED + when /transaction\s*\Z/i + CYAN + else + MAGENTA + end + end + + def logger + ActiveRecord::Base.logger + end + + def type_cast(value) + ActiveRecord::Base.connection.type_cast(value) + end end end diff --git a/activerecord/lib/active_record/migration.rb b/activerecord/lib/active_record/migration.rb index 4cfda302ea..ed0c81b639 100644 --- a/activerecord/lib/active_record/migration.rb +++ b/activerecord/lib/active_record/migration.rb @@ -1,5 +1,6 @@ +require "set" +require "zlib" require "active_support/core_ext/module/attribute_accessors" -require 'set' module ActiveRecord class MigrationError < ActiveRecordError#:nodoc: @@ -9,40 +10,170 @@ module ActiveRecord end end - # Exception that can be raised to stop migrations from going backwards. + # Exception that can be raised to stop migrations from being rolled back. + # For example the following migration is not reversible. + # Rolling back this migration will raise an ActiveRecord::IrreversibleMigration error. + # + # class IrreversibleMigrationExample < ActiveRecord::Migration[5.0] + # def change + # create_table :distributors do |t| + # t.string :zipcode + # end + # + # execute <<-SQL + # ALTER TABLE distributors + # ADD CONSTRAINT zipchk + # CHECK (char_length(zipcode) = 5) NO INHERIT; + # SQL + # end + # end + # + # There are two ways to mitigate this problem. + # + # 1. Define <tt>#up</tt> and <tt>#down</tt> methods instead of <tt>#change</tt>: + # + # class ReversibleMigrationExample < ActiveRecord::Migration[5.0] + # def up + # create_table :distributors do |t| + # t.string :zipcode + # end + # + # execute <<-SQL + # ALTER TABLE distributors + # ADD CONSTRAINT zipchk + # CHECK (char_length(zipcode) = 5) NO INHERIT; + # SQL + # end + # + # def down + # execute <<-SQL + # ALTER TABLE distributors + # DROP CONSTRAINT zipchk + # SQL + # + # drop_table :distributors + # end + # end + # + # 2. Use the #reversible method in <tt>#change</tt> method: + # + # class ReversibleMigrationExample < ActiveRecord::Migration[5.0] + # def change + # create_table :distributors do |t| + # t.string :zipcode + # end + # + # reversible do |dir| + # dir.up do + # execute <<-SQL + # ALTER TABLE distributors + # ADD CONSTRAINT zipchk + # CHECK (char_length(zipcode) = 5) NO INHERIT; + # SQL + # end + # + # dir.down do + # execute <<-SQL + # ALTER TABLE distributors + # DROP CONSTRAINT zipchk + # SQL + # end + # end + # end + # end class IrreversibleMigration < MigrationError end class DuplicateMigrationVersionError < MigrationError#:nodoc: - def initialize(version) - super("Multiple migrations have the version number #{version}") + def initialize(version = nil) + if version + super("Multiple migrations have the version number #{version}.") + else + super("Duplicate migration version error.") + end end end class DuplicateMigrationNameError < MigrationError#:nodoc: - def initialize(name) - super("Multiple migrations have the name #{name}") + def initialize(name = nil) + if name + super("Multiple migrations have the name #{name}.") + else + super("Duplicate migration name.") + end end end class UnknownMigrationVersionError < MigrationError #:nodoc: - def initialize(version) - super("No migration with version number #{version}") + def initialize(version = nil) + if version + super("No migration with version number #{version}.") + else + super("Unknown migration version.") + end end end class IllegalMigrationNameError < MigrationError#:nodoc: - def initialize(name) - super("Illegal name for migration file: #{name}\n\t(only lower case letters, numbers, and '_' allowed)") + def initialize(name = nil) + if name + super("Illegal name for migration file: #{name}\n\t(only lower case letters, numbers, and '_' allowed).") + else + super("Illegal name for migration.") + end end end class PendingMigrationError < MigrationError#:nodoc: + def initialize(message = nil) + if !message && defined?(Rails.env) + super("Migrations are pending. To resolve this issue, run:\n\n bin/rails db:migrate RAILS_ENV=#{::Rails.env}") + elsif !message + super("Migrations are pending. To resolve this issue, run:\n\n bin/rails db:migrate") + else + super + end + end + end + + class ConcurrentMigrationError < MigrationError #:nodoc: + DEFAULT_MESSAGE = "Cannot run migrations because another migration process is currently running.".freeze + + def initialize(message = DEFAULT_MESSAGE) + super + end + end + + class NoEnvironmentInSchemaError < MigrationError #:nodoc: def initialize + msg = "Environment data not found in the schema. To resolve this issue, run: \n\n bin/rails db:environment:set" + if defined?(Rails.env) + super("#{msg} RAILS_ENV=#{::Rails.env}") + else + super(msg) + end + end + end + + class ProtectedEnvironmentError < ActiveRecordError #:nodoc: + def initialize(env = "production") + msg = "You are attempting to run a destructive action against your '#{env}' database.\n" + msg << "If you are sure you want to continue, run the same command with the environment variable:\n" + msg << "DISABLE_DATABASE_ENVIRONMENT_CHECK=1" + super(msg) + end + end + + class EnvironmentMismatchError < ActiveRecordError + def initialize(current: nil, stored: nil) + msg = "You are attempting to modify a database that was last run in `#{ stored }` environment.\n" + msg << "You are running in `#{ current }` environment. " + msg << "If you are sure you want to continue, first set the environment using:\n\n" + msg << " bin/rails db:environment:set" if defined?(Rails.env) - super("Migrations are pending. To resolve this issue, run:\n\n\tbin/rake db:migrate RAILS_ENV=#{::Rails.env}") + super("#{msg} RAILS_ENV=#{::Rails.env}\n\n") else - super("Migrations are pending. To resolve this issue, run:\n\n\tbin/rake db:migrate") + super("#{msg}\n\n") end end end @@ -59,7 +190,7 @@ module ActiveRecord # # Example of a simple migration: # - # class AddSsl < ActiveRecord::Migration + # class AddSsl < ActiveRecord::Migration[5.0] # def up # add_column :accounts, :ssl_enabled, :boolean, default: true # end @@ -79,7 +210,7 @@ module ActiveRecord # # Example of a more complex migration that also needs to initialize data: # - # class AddSystemSettings < ActiveRecord::Migration + # class AddSystemSettings < ActiveRecord::Migration[5.0] # def up # create_table :system_settings do |t| # t.string :name @@ -106,17 +237,18 @@ module ActiveRecord # # == Available transformations # + # === Creation + # + # * <tt>create_join_table(table_1, table_2, options)</tt>: Creates a join + # table having its name as the lexical order of the first two + # arguments. See + # ActiveRecord::ConnectionAdapters::SchemaStatements#create_join_table for + # details. # * <tt>create_table(name, options)</tt>: Creates a table called +name+ and # makes the table object available to a block that can then add columns to it, # following the same format as +add_column+. See example above. The options hash # is for fragments like "DEFAULT CHARSET=UTF-8" that are appended to the create # table definition. - # * <tt>drop_table(name)</tt>: Drops the table called +name+. - # * <tt>change_table(name, options)</tt>: Allows to make column alterations to - # the table called +name+. It makes the table object available to a block that - # can then add/remove columns, indexes or foreign keys to it. - # * <tt>rename_table(old_name, new_name)</tt>: Renames the table called +old_name+ - # to +new_name+. # * <tt>add_column(table_name, column_name, type, options)</tt>: Adds a new column # to the table called +table_name+ # named +column_name+ specified to be one of the following types: @@ -127,24 +259,61 @@ module ActiveRecord # Other options include <tt>:limit</tt> and <tt>:null</tt> (e.g. # <tt>{ limit: 50, null: false }</tt>) -- see # ActiveRecord::ConnectionAdapters::TableDefinition#column for details. - # * <tt>rename_column(table_name, column_name, new_column_name)</tt>: Renames - # a column but keeps the type and content. - # * <tt>change_column(table_name, column_name, type, options)</tt>: Changes - # the column to a different type using the same parameters as add_column. - # * <tt>remove_column(table_name, column_name, type, options)</tt>: Removes the column - # named +column_name+ from the table called +table_name+. + # * <tt>add_foreign_key(from_table, to_table, options)</tt>: Adds a new + # foreign key. +from_table+ is the table with the key column, +to_table+ contains + # the referenced primary key. # * <tt>add_index(table_name, column_names, options)</tt>: Adds a new index # with the name of the column. Other options include # <tt>:name</tt>, <tt>:unique</tt> (e.g. # <tt>{ name: 'users_name_index', unique: true }</tt>) and <tt>:order</tt> # (e.g. <tt>{ order: { name: :desc } }</tt>). + # * <tt>add_reference(:table_name, :reference_name)</tt>: Adds a new column + # +reference_name_id+ by default an integer. See + # ActiveRecord::ConnectionAdapters::SchemaStatements#add_reference for details. + # * <tt>add_timestamps(table_name, options)</tt>: Adds timestamps (+created_at+ + # and +updated_at+) columns to +table_name+. + # + # === Modification + # + # * <tt>change_column(table_name, column_name, type, options)</tt>: Changes + # the column to a different type using the same parameters as add_column. + # * <tt>change_column_default(table_name, column_name, default_or_changes)</tt>: + # Sets a default value for +column_name+ defined by +default_or_changes+ on + # +table_name+. Passing a hash containing <tt>:from</tt> and <tt>:to</tt> + # as +default_or_changes+ will make this change reversible in the migration. + # * <tt>change_column_null(table_name, column_name, null, default = nil)</tt>: + # Sets or removes a +NOT NULL+ constraint on +column_name+. The +null+ flag + # indicates whether the value can be +NULL+. See + # ActiveRecord::ConnectionAdapters::SchemaStatements#change_column_null for + # details. + # * <tt>change_table(name, options)</tt>: Allows to make column alterations to + # the table called +name+. It makes the table object available to a block that + # can then add/remove columns, indexes or foreign keys to it. + # * <tt>rename_column(table_name, column_name, new_column_name)</tt>: Renames + # a column but keeps the type and content. + # * <tt>rename_index(table_name, old_name, new_name)</tt>: Renames an index. + # * <tt>rename_table(old_name, new_name)</tt>: Renames the table called +old_name+ + # to +new_name+. + # + # === Deletion + # + # * <tt>drop_table(name)</tt>: Drops the table called +name+. + # * <tt>drop_join_table(table_1, table_2, options)</tt>: Drops the join table + # specified by the given arguments. + # * <tt>remove_column(table_name, column_name, type, options)</tt>: Removes the column + # named +column_name+ from the table called +table_name+. + # * <tt>remove_columns(table_name, *column_names)</tt>: Removes the given + # columns from the table definition. + # * <tt>remove_foreign_key(from_table, options_or_to_table)</tt>: Removes the + # given foreign key from the table called +table_name+. # * <tt>remove_index(table_name, column: column_names)</tt>: Removes the index # specified by +column_names+. # * <tt>remove_index(table_name, name: index_name)</tt>: Removes the index # specified by +index_name+. - # * <tt>add_reference(:table_name, :reference_name)</tt>: Adds a new column - # +reference_name_id+ by default a integer. See - # ActiveRecord::ConnectionAdapters::SchemaStatements#add_reference for details. + # * <tt>remove_reference(table_name, ref_name, options)</tt>: Removes the + # reference(s) on +table_name+ specified by +ref_name+. + # * <tt>remove_timestamps(table_name, options)</tt>: Removes the timestamp + # columns (+created_at+ and +updated_at+) from the table definition. # # == Irreversible transformations # @@ -168,24 +337,24 @@ module ActiveRecord # # rails generate migration add_fieldname_to_tablename fieldname:string # - # This will generate the file <tt>timestamp_add_fieldname_to_tablename</tt>, which will look like this: - # class AddFieldnameToTablename < ActiveRecord::Migration + # This will generate the file <tt>timestamp_add_fieldname_to_tablename.rb</tt>, which will look like this: + # class AddFieldnameToTablename < ActiveRecord::Migration[5.0] # def change # add_column :tablenames, :fieldname, :string # end # end # # To run migrations against the currently configured database, use - # <tt>rake db:migrate</tt>. This will update the database by running all of the + # <tt>rails db:migrate</tt>. This will update the database by running all of the # pending migrations, creating the <tt>schema_migrations</tt> table # (see "About the schema_migrations table" section below) if missing. It will also # invoke the db:schema:dump task, which will update your db/schema.rb file # to match the structure of your database. # # To roll the database back to a previous migration version, use - # <tt>rake db:migrate VERSION=X</tt> where <tt>X</tt> is the version to which + # <tt>rails db:migrate VERSION=X</tt> where <tt>X</tt> is the version to which # you wish to downgrade. Alternatively, you can also use the STEP option if you - # wish to rollback last few migrations. <tt>rake db:migrate STEP=2</tt> will rollback + # wish to rollback last few migrations. <tt>rails db:migrate STEP=2</tt> will rollback # the latest two migrations. # # If any of the migrations throw an <tt>ActiveRecord::IrreversibleMigration</tt> exception, @@ -200,7 +369,7 @@ module ActiveRecord # # Not all migrations change the schema. Some just fix the data: # - # class RemoveEmptyTags < ActiveRecord::Migration + # class RemoveEmptyTags < ActiveRecord::Migration[5.0] # def up # Tag.all.each { |tag| tag.destroy if tag.pages.empty? } # end @@ -213,7 +382,7 @@ module ActiveRecord # # Others remove columns when they migrate up instead of down: # - # class RemoveUnnecessaryItemAttributes < ActiveRecord::Migration + # class RemoveUnnecessaryItemAttributes < ActiveRecord::Migration[5.0] # def up # remove_column :items, :incomplete_items_count # remove_column :items, :completed_items_count @@ -227,7 +396,7 @@ module ActiveRecord # # And sometimes you need to do something in SQL not abstracted directly by migrations: # - # class MakeJoinUnique < ActiveRecord::Migration + # class MakeJoinUnique < ActiveRecord::Migration[5.0] # def up # execute "ALTER TABLE `pages_linked_pages` ADD UNIQUE `page_id_linked_page_id` (`page_id`,`linked_page_id`)" # end @@ -244,7 +413,7 @@ module ActiveRecord # <tt>Base#reset_column_information</tt> in order to ensure that the model has the # latest column data from after the new column was added. Example: # - # class AddPeopleSalary < ActiveRecord::Migration + # class AddPeopleSalary < ActiveRecord::Migration[5.0] # def up # add_column :people, :salary, :integer # Person.reset_column_information @@ -302,7 +471,7 @@ module ActiveRecord # To define a reversible migration, define the +change+ method in your # migration like this: # - # class TenderloveMigration < ActiveRecord::Migration + # class TenderloveMigration < ActiveRecord::Migration[5.0] # def change # create_table(:horses) do |t| # t.column :content, :text @@ -332,7 +501,7 @@ module ActiveRecord # can't execute inside a transaction though, and for these situations # you can turn the automatic transactions off. # - # class ChangeEnum < ActiveRecord::Migration + # class ChangeEnum < ActiveRecord::Migration[5.0] # disable_ddl_transaction! # # def up @@ -343,11 +512,35 @@ module ActiveRecord # Remember that you can still open your own transactions, even if you # are in a Migration with <tt>self.disable_ddl_transaction!</tt>. class Migration - autoload :CommandRecorder, 'active_record/migration/command_recorder' + autoload :CommandRecorder, "active_record/migration/command_recorder" + autoload :Compatibility, "active_record/migration/compatibility" + + # This must be defined before the inherited hook, below + class Current < Migration # :nodoc: + end + + def self.inherited(subclass) # :nodoc: + super + if subclass.superclass == Migration + raise StandardError, "Directly inheriting from ActiveRecord::Migration is not supported. " \ + "Please specify the Rails release the migration was written for:\n" \ + "\n" \ + " class #{self.class.name} < ActiveRecord::Migration[4.2]" + end + end + + def self.[](version) + Compatibility.find(version) + end + + def self.current_version + ActiveRecord::VERSION::STRING.to_f + end + MigrationFilenameRegexp = /\A([0-9]+)_([_a-z0-9]*)\.?([_a-z0-9]*)?\.rb\z/ # :nodoc: # This class is used to verify that all migrations have been run before - # loading a web page if config.active_record.migration_error is set to :page_load + # loading a web page if <tt>config.active_record.migration_error</tt> is set to :page_load class CheckPending def initialize(app) @app = app @@ -367,15 +560,20 @@ module ActiveRecord private - def connection - ActiveRecord::Base.connection - end + def connection + ActiveRecord::Base.connection + end end class << self attr_accessor :delegate # :nodoc: attr_accessor :disable_ddl_transaction # :nodoc: + def nearest_delegate # :nodoc: + delegate || superclass.nearest_delegate + end + + # Raises <tt>ActiveRecord::PendingMigrationError</tt> error if any migrations are pending. def check_pending!(connection = Base.connection) raise ActiveRecord::PendingMigrationError if ActiveRecord::Migrator.needs_migration?(connection) end @@ -386,7 +584,7 @@ module ActiveRecord FileUtils.cd Rails.root do current_config = Base.connection_config Base.clear_all_connections! - system("bin/rake db:test:prepare") + system("bin/rails db:test:prepare") # Establish a new connection, the old database may be gone (db:test:prepare uses purge) Base.establish_connection(current_config) end @@ -401,14 +599,17 @@ module ActiveRecord end def method_missing(name, *args, &block) # :nodoc: - (delegate || superclass.delegate).send(name, *args, &block) + nearest_delegate.send(name, *args, &block) end def migrate(direction) new.migrate direction end - # Disable DDL transactions for this migration. + # Disable the transaction wrapping this migration. + # You can still create your own transactions even after calling #disable_ddl_transaction! + # + # For more details read the {"Transactional Migrations" section above}[rdoc-ref:Migration]. def disable_ddl_transaction! @disable_ddl_transaction = true end @@ -438,7 +639,7 @@ module ActiveRecord # and create the table 'apples' on the way up, and the reverse # on the way down. # - # class FixTLMigration < ActiveRecord::Migration + # class FixTLMigration < ActiveRecord::Migration[5.0] # def change # revert do # create_table(:horses) do |t| @@ -455,9 +656,9 @@ module ActiveRecord # Or equivalently, if +TenderloveMigration+ is defined as in the # documentation for Migration: # - # require_relative '2012121212_tenderlove_migration' + # require_relative '20121212123456_tenderlove_migration' # - # class FixupTLMigration < ActiveRecord::Migration + # class FixupTLMigration < ActiveRecord::Migration[5.0] # def change # revert TenderloveMigration # @@ -471,13 +672,13 @@ module ActiveRecord def revert(*migration_classes) run(*migration_classes.reverse, revert: true) unless migration_classes.empty? if block_given? - if @connection.respond_to? :revert - @connection.revert { yield } + if connection.respond_to? :revert + connection.revert { yield } else - recorder = CommandRecorder.new(@connection) + recorder = CommandRecorder.new(connection) @connection = recorder suppress_messages do - @connection.revert { yield } + connection.revert { yield } end @connection = recorder.delegate recorder.commands.each do |cmd, args, block| @@ -488,7 +689,7 @@ module ActiveRecord end def reverting? - @connection.respond_to?(:reverting) && @connection.reverting + connection.respond_to?(:reverting) && connection.reverting end class ReversibleBlockHelper < Struct.new(:reverting) # :nodoc: @@ -510,7 +711,7 @@ module ActiveRecord # when the three columns 'first_name', 'last_name' and 'full_name' exist, # even when migrating down: # - # class SplitNameMigration < ActiveRecord::Migration + # class SplitNameMigration < ActiveRecord::Migration[5.0] # def change # add_column :users, :first_name, :string # add_column :users, :last_name, :string @@ -529,7 +730,7 @@ module ActiveRecord # end def reversible helper = ReversibleBlockHelper.new(reverting?) - execute_block{ yield helper } + execute_block { yield helper } end # Runs the given migration classes. @@ -545,7 +746,7 @@ module ActiveRecord revert { run(*migration_classes, direction: dir, revert: true) } else migration_classes.each do |migration_class| - migration_class.new.exec_migration(@connection, dir) + migration_class.new.exec_migration(connection, dir) end end end @@ -571,7 +772,7 @@ module ActiveRecord when :down then announce "reverting" end - time = nil + time = nil ActiveRecord::Base.connection_pool.with_connection do |conn| time = Benchmark.measure do exec_migration(conn, direction) @@ -599,7 +800,7 @@ module ActiveRecord @connection = nil end - def write(text="") + def write(text = "") puts(text) if verbose end @@ -609,7 +810,7 @@ module ActiveRecord write "== %s %s" % [text, "=" * length] end - def say(message, subitem=false) + def say(message, subitem = false) write "#{subitem ? " ->" : "--"} #{message}" end @@ -634,10 +835,10 @@ module ActiveRecord end def method_missing(method, *arguments, &block) - arg_list = arguments.map(&:inspect) * ', ' + arg_list = arguments.map(&:inspect) * ", " say_with_time "#{method}(#{arg_list})" do - unless @connection.respond_to? :revert + unless connection.respond_to? :revert unless arguments.empty? || [:execute, :enable_extension, :disable_extension].include?(method) arguments[0] = proper_table_name(arguments.first, table_name_options) if [:rename_table, :add_foreign_key].include?(method) || @@ -716,7 +917,9 @@ module ActiveRecord end end - def table_name_options(config = ActiveRecord::Base) + # Builds a hash for use in ActiveRecord::Migration#proper_table_name using + # the Active Record object's table_name prefix and suffix + def table_name_options(config = ActiveRecord::Base) #:nodoc: { table_name_prefix: config.table_name_prefix, table_name_suffix: config.table_name_suffix @@ -724,19 +927,18 @@ module ActiveRecord end private - def execute_block - if connection.respond_to? :execute_block - super # use normal delegation to record the block - else - yield + def execute_block + if connection.respond_to? :execute_block + super # use normal delegation to record the block + else + yield + end end - end end # MigrationProxy is used to defer loading of the actual migration classes # until they are needed class MigrationProxy < Struct.new(:name, :version, :filename, :scope) - def initialize(name, version, filename, scope) super @migration = nil @@ -762,7 +964,6 @@ module ActiveRecord require(File.expand_path(filename)) name.constantize.new(name, version) end - end class NullMigration < MigrationProxy #:nodoc: @@ -793,11 +994,11 @@ module ActiveRecord end end - def rollback(migrations_paths, steps=1) + def rollback(migrations_paths, steps = 1) move(:down, migrations_paths, steps) end - def forward(migrations_paths, steps=1) + def forward(migrations_paths, steps = 1) move(:up, migrations_paths, steps) end @@ -808,7 +1009,7 @@ module ActiveRecord new(:up, migrations, target_version).migrate end - def down(migrations_paths, target_version = nil, &block) + def down(migrations_paths, target_version = nil) migrations = migrations(migrations_paths) migrations.select! { |m| yield m } if block_given? @@ -847,22 +1048,22 @@ module ActiveRecord migrations(migrations_paths).any? end - def last_version - last_migration.version - end - def last_migration #:nodoc: migrations(migrations_paths).last || NullMigration.new end def migrations_paths - @migrations_paths ||= ['db/migrate'] - # just to not break things if someone uses: migration_path = some_string + @migrations_paths ||= ["db/migrate"] + # just to not break things if someone uses: migrations_path = some_string Array(@migrations_paths) end - def migrations_path - migrations_paths.first + def match_to_migration_filename?(filename) # :nodoc: + Migration::MigrationFilenameRegexp.match?(File.basename(filename)) + end + + def parse_migration_filename(filename) # :nodoc: + File.basename(filename).scan(Migration::MigrationFilenameRegexp).first end def migrations(paths) @@ -871,8 +1072,7 @@ module ActiveRecord files = Dir[*paths.map { |p| "#{p}/**/[0-9]*_*.rb" }] migrations = files.map do |file| - version, name, scope = file.scan(/([0-9]+)_([_a-z0-9]*)\.?([_a-z0-9]*)?\.rb\z/).first - + version, name, scope = parse_migration_filename(file) raise IllegalMigrationNameError.new(file) unless version version = version.to_i name = name.camelize @@ -908,6 +1108,7 @@ module ActiveRecord validate(@migrations) Base.connection.initialize_schema_migrations_table + Base.connection.initialize_internal_metadata_table end def current_version @@ -920,32 +1121,18 @@ module ActiveRecord alias :current :current_migration def run - migration = migrations.detect { |m| m.version == @target_version } - raise UnknownMigrationVersionError.new(@target_version) if migration.nil? - unless (up? && migrated.include?(migration.version.to_i)) || (down? && !migrated.include?(migration.version.to_i)) - begin - execute_migration_in_transaction(migration, @direction) - rescue => e - canceled_msg = use_transaction?(migration) ? ", this migration was canceled" : "" - raise StandardError, "An error has occurred#{canceled_msg}:\n\n#{e}", e.backtrace - end + if use_advisory_lock? + with_advisory_lock { run_without_lock } + else + run_without_lock end end def migrate - if !target && @target_version && @target_version > 0 - raise UnknownMigrationVersionError.new(@target_version) - end - - runnable.each do |migration| - Base.logger.info "Migrating to #{migration.name} (#{migration.version})" if Base.logger - - begin - execute_migration_in_transaction(migration, @direction) - rescue => e - canceled_msg = use_transaction?(migration) ? "this and " : "" - raise StandardError, "An error has occurred, #{canceled_msg}all later migrations canceled:\n\n#{e}", e.backtrace - end + if use_advisory_lock? + with_advisory_lock { migrate_without_lock } + else + migrate_without_lock end end @@ -970,70 +1157,155 @@ module ActiveRecord end def migrated - @migrated_versions ||= Set.new(self.class.get_all_versions) + @migrated_versions || load_migrated end - private - def ran?(migration) - migrated.include?(migration.version.to_i) + def load_migrated + @migrated_versions = Set.new(self.class.get_all_versions) end - def execute_migration_in_transaction(migration, direction) - ddl_transaction(migration) do - migration.migrate(direction) - record_version_state_after_migrating(migration.version) + private + + # Used for running a specific migration. + def run_without_lock + migration = migrations.detect { |m| m.version == @target_version } + raise UnknownMigrationVersionError.new(@target_version) if migration.nil? + execute_migration_in_transaction(migration, @direction) + + record_environment end - end - def target - migrations.detect { |m| m.version == @target_version } - end + # Used for running multiple migrations up to or down to a certain value. + def migrate_without_lock + if invalid_target? + raise UnknownMigrationVersionError.new(@target_version) + end - def finish - migrations.index(target) || migrations.size - 1 - end + runnable.each do |migration| + execute_migration_in_transaction(migration, @direction) + end - def start - up? ? 0 : (migrations.index(current) || 0) - end + record_environment + end - def validate(migrations) - name ,= migrations.group_by(&:name).find { |_,v| v.length > 1 } - raise DuplicateMigrationNameError.new(name) if name + # Stores the current environment in the database. + def record_environment + return if down? + ActiveRecord::InternalMetadata[:environment] = ActiveRecord::Migrator.current_environment + end - version ,= migrations.group_by(&:version).find { |_,v| v.length > 1 } - raise DuplicateMigrationVersionError.new(version) if version - end + def ran?(migration) + migrated.include?(migration.version.to_i) + end - def record_version_state_after_migrating(version) - if down? - migrated.delete(version) - ActiveRecord::SchemaMigration.where(:version => version.to_s).delete_all - else - migrated << version - ActiveRecord::SchemaMigration.create!(:version => version.to_s) + # Return true if a valid version is not provided. + def invalid_target? + !target && @target_version && @target_version > 0 end - end - def up? - @direction == :up - end + def execute_migration_in_transaction(migration, direction) + return if down? && !migrated.include?(migration.version.to_i) + return if up? && migrated.include?(migration.version.to_i) - def down? - @direction == :down - end + Base.logger.info "Migrating to #{migration.name} (#{migration.version})" if Base.logger - # Wrap the migration in a transaction only if supported by the adapter. - def ddl_transaction(migration) - if use_transaction?(migration) - Base.transaction { yield } - else + ddl_transaction(migration) do + migration.migrate(direction) + record_version_state_after_migrating(migration.version) + end + rescue => e + msg = "An error has occurred, " + msg << "this and " if use_transaction?(migration) + msg << "all later migrations canceled:\n\n#{e}" + raise StandardError, msg, e.backtrace + end + + def target + migrations.detect { |m| m.version == @target_version } + end + + def finish + migrations.index(target) || migrations.size - 1 + end + + def start + up? ? 0 : (migrations.index(current) || 0) + end + + def validate(migrations) + name , = migrations.group_by(&:name).find { |_, v| v.length > 1 } + raise DuplicateMigrationNameError.new(name) if name + + version , = migrations.group_by(&:version).find { |_, v| v.length > 1 } + raise DuplicateMigrationVersionError.new(version) if version + end + + def record_version_state_after_migrating(version) + if down? + migrated.delete(version) + ActiveRecord::SchemaMigration.where(version: version.to_s).delete_all + else + migrated << version + ActiveRecord::SchemaMigration.create!(version: version.to_s) + end + end + + def self.last_stored_environment + return nil if current_version == 0 + raise NoEnvironmentInSchemaError unless ActiveRecord::InternalMetadata.table_exists? + + environment = ActiveRecord::InternalMetadata[:environment] + raise NoEnvironmentInSchemaError unless environment + environment + end + + def self.current_environment + ActiveRecord::ConnectionHandling::DEFAULT_ENV.call + end + + def self.protected_environment? + ActiveRecord::Base.protected_environments.include?(last_stored_environment) if last_stored_environment + end + + def up? + @direction == :up + end + + def down? + @direction == :down + end + + # Wrap the migration in a transaction only if supported by the adapter. + def ddl_transaction(migration) + if use_transaction?(migration) + Base.transaction { yield } + else + yield + end + end + + def use_transaction?(migration) + !migration.disable_ddl_transaction && Base.connection.supports_ddl_transactions? + end + + def use_advisory_lock? + Base.connection.supports_advisory_locks? + end + + def with_advisory_lock + lock_id = generate_migrator_advisory_lock_id + got_lock = Base.connection.get_advisory_lock(lock_id) + raise ConcurrentMigrationError unless got_lock + load_migrated # reload schema_migrations to be sure it wasn't changed by another process before we got the lock yield + ensure + Base.connection.release_advisory_lock(lock_id) if got_lock end - end - def use_transaction?(migration) - !migration.disable_ddl_transaction && Base.connection.supports_ddl_transactions? - end + MIGRATOR_SALT = 2053462845 + def generate_migrator_advisory_lock_id + db_name_hash = Zlib.crc32(Base.connection.current_database) + MIGRATOR_SALT * db_name_hash + end end end diff --git a/activerecord/lib/active_record/migration/command_recorder.rb b/activerecord/lib/active_record/migration/command_recorder.rb index dcc2362397..03103bba98 100644 --- a/activerecord/lib/active_record/migration/command_recorder.rb +++ b/activerecord/lib/active_record/migration/command_recorder.rb @@ -5,15 +5,36 @@ module ActiveRecord # knows how to invert the following commands: # # * add_column + # * add_foreign_key # * add_index + # * add_reference # * add_timestamps - # * create_table + # * change_column + # * change_column_default (must supply a :from and :to option) + # * change_column_null # * create_join_table + # * create_table + # * disable_extension + # * drop_join_table + # * drop_table (must supply a block) + # * enable_extension + # * remove_column (must supply a type) + # * remove_columns (must specify at least one column name or more) + # * remove_foreign_key (must supply a second table) + # * remove_index + # * remove_reference # * remove_timestamps # * rename_column # * rename_index # * rename_table class CommandRecorder + ReversibleAndIrreversibleMethods = [:create_table, :create_join_table, :rename_table, :add_column, :remove_column, + :rename_index, :rename_column, :add_index, :remove_index, :add_timestamps, :remove_timestamps, + :change_column_default, :add_reference, :remove_reference, :transaction, + :drop_join_table, :drop_table, :execute_block, :enable_extension, :disable_extension, + :change_column, :execute, :remove_columns, :change_column_null, + :add_foreign_key, :remove_foreign_key + ] include JoinTable attr_accessor :commands, :delegate, :reverting @@ -41,7 +62,7 @@ module ActiveRecord @reverting = !@reverting end - # record +command+. +command+ should be a method name and arguments. + # Record +command+. +command+ should be a method name and arguments. # For example: # # recorder.record(:method_name, [:arg1, :arg2]) @@ -62,22 +83,20 @@ module ActiveRecord # invert the +command+. def inverse_of(command, args, &block) method = :"invert_#{command}" - raise IrreversibleMigration unless respond_to?(method, true) + raise IrreversibleMigration, <<-MSG.strip_heredoc unless respond_to?(method, true) + This migration uses #{command}, which is not automatically reversible. + To make the migration reversible you can either: + 1. Define #up and #down methods in place of the #change method. + 2. Use the #reversible method to define reversible behavior. + MSG send(method, args, &block) end - def respond_to?(*args) # :nodoc: + def respond_to_missing?(*args) # :nodoc: super || delegate.respond_to?(*args) end - [:create_table, :create_join_table, :rename_table, :add_column, :remove_column, - :rename_index, :rename_column, :add_index, :remove_index, :add_timestamps, :remove_timestamps, - :add_reference, :remove_reference, :transaction, - :drop_join_table, :drop_table, :execute_block, :enable_extension, - :change_column, :execute, :remove_columns, :change_column_null, - :add_foreign_key, :remove_foreign_key - # irreversible methods need to be here too - ].each do |method| + ReversibleAndIrreversibleMethods.each do |method| class_eval <<-EOV, __FILE__, __LINE__ + 1 def #{method}(*args, &block) # def create_table(*args, &block) record(:"#{method}", args, &block) # record(:create_table, args, &block) @@ -93,127 +112,127 @@ module ActiveRecord private - module StraightReversions - private - { transaction: :transaction, - execute_block: :execute_block, - create_table: :drop_table, - create_join_table: :drop_join_table, - add_column: :remove_column, - add_timestamps: :remove_timestamps, - add_reference: :remove_reference, - enable_extension: :disable_extension - }.each do |cmd, inv| - [[inv, cmd], [cmd, inv]].uniq.each do |method, inverse| - class_eval <<-EOV, __FILE__, __LINE__ + 1 - def invert_#{method}(args, &block) # def invert_create_table(args, &block) - [:#{inverse}, args, block] # [:drop_table, args, block] - end # end - EOV - end + module StraightReversions + private + { transaction: :transaction, + execute_block: :execute_block, + create_table: :drop_table, + create_join_table: :drop_join_table, + add_column: :remove_column, + add_timestamps: :remove_timestamps, + add_reference: :remove_reference, + enable_extension: :disable_extension + }.each do |cmd, inv| + [[inv, cmd], [cmd, inv]].uniq.each do |method, inverse| + class_eval <<-EOV, __FILE__, __LINE__ + 1 + def invert_#{method}(args, &block) # def invert_create_table(args, &block) + [:#{inverse}, args, block] # [:drop_table, args, block] + end # end + EOV + end + end end - end - include StraightReversions + include StraightReversions - def invert_drop_table(args, &block) - if args.size == 1 && block == nil - raise ActiveRecord::IrreversibleMigration, "To avoid mistakes, drop_table is only reversible if given options or a block (can be empty)." + def invert_drop_table(args, &block) + if args.size == 1 && block == nil + raise ActiveRecord::IrreversibleMigration, "To avoid mistakes, drop_table is only reversible if given options or a block (can be empty)." + end + super end - super - end - def invert_rename_table(args) - [:rename_table, args.reverse] - end + def invert_rename_table(args) + [:rename_table, args.reverse] + end - def invert_remove_column(args) - raise ActiveRecord::IrreversibleMigration, "remove_column is only reversible if given a type." if args.size <= 2 - super - end + def invert_remove_column(args) + raise ActiveRecord::IrreversibleMigration, "remove_column is only reversible if given a type." if args.size <= 2 + super + end - def invert_rename_index(args) - [:rename_index, [args.first] + args.last(2).reverse] - end + def invert_rename_index(args) + [:rename_index, [args.first] + args.last(2).reverse] + end - def invert_rename_column(args) - [:rename_column, [args.first] + args.last(2).reverse] - end + def invert_rename_column(args) + [:rename_column, [args.first] + args.last(2).reverse] + end - def invert_add_index(args) - table, columns, options = *args - options ||= {} + def invert_add_index(args) + table, columns, options = *args + options ||= {} - index_name = options[:name] - options_hash = index_name ? { name: index_name } : { column: columns } + index_name = options[:name] + options_hash = index_name ? { name: index_name } : { column: columns } - [:remove_index, [table, options_hash]] - end + [:remove_index, [table, options_hash]] + end - def invert_remove_index(args) - table, options_or_column = *args - if (options = options_or_column).is_a?(Hash) - unless options[:column] - raise ActiveRecord::IrreversibleMigration, "remove_index is only reversible if given a :column option." + def invert_remove_index(args) + table, options_or_column = *args + if (options = options_or_column).is_a?(Hash) + unless options[:column] + raise ActiveRecord::IrreversibleMigration, "remove_index is only reversible if given a :column option." + end + options = options.dup + [:add_index, [table, options.delete(:column), options]] + elsif (column = options_or_column).present? + [:add_index, [table, column]] end - options = options.dup - [:add_index, [table, options.delete(:column), options]] - elsif (column = options_or_column).present? - [:add_index, [table, column]] end - end - alias :invert_add_belongs_to :invert_add_reference - alias :invert_remove_belongs_to :invert_remove_reference + alias :invert_add_belongs_to :invert_add_reference + alias :invert_remove_belongs_to :invert_remove_reference - def invert_change_column_default(args) - table, column, options = *args + def invert_change_column_default(args) + table, column, options = *args - unless options && options.is_a?(Hash) && options.has_key?(:from) && options.has_key?(:to) - raise ActiveRecord::IrreversibleMigration, "change_column_default is only reversible if given a :from and :to option." + unless options && options.is_a?(Hash) && options.has_key?(:from) && options.has_key?(:to) + raise ActiveRecord::IrreversibleMigration, "change_column_default is only reversible if given a :from and :to option." + end + + [:change_column_default, [table, column, from: options[:to], to: options[:from]]] end - [:change_column_default, [table, column, from: options[:to], to: options[:from]]] - end + def invert_change_column_null(args) + args[2] = !args[2] + [:change_column_null, args] + end - def invert_change_column_null(args) - args[2] = !args[2] - [:change_column_null, args] - end + def invert_add_foreign_key(args) + from_table, to_table, add_options = args + add_options ||= {} - def invert_add_foreign_key(args) - from_table, to_table, add_options = args - add_options ||= {} + if add_options[:name] + options = { name: add_options[:name] } + elsif add_options[:column] + options = { column: add_options[:column] } + else + options = to_table + end - if add_options[:name] - options = { name: add_options[:name] } - elsif add_options[:column] - options = { column: add_options[:column] } - else - options = to_table + [:remove_foreign_key, [from_table, options]] end - [:remove_foreign_key, [from_table, options]] - end + def invert_remove_foreign_key(args) + from_table, to_table, remove_options = args + raise ActiveRecord::IrreversibleMigration, "remove_foreign_key is only reversible if given a second table" if to_table.nil? || to_table.is_a?(Hash) - def invert_remove_foreign_key(args) - from_table, to_table, remove_options = args - raise ActiveRecord::IrreversibleMigration, "remove_foreign_key is only reversible if given a second table" if to_table.nil? || to_table.is_a?(Hash) + reversed_args = [from_table, to_table] + reversed_args << remove_options if remove_options - reversed_args = [from_table, to_table] - reversed_args << remove_options if remove_options - - [:add_foreign_key, reversed_args] - end + [:add_foreign_key, reversed_args] + end - # Forwards any missing method call to the \target. - def method_missing(method, *args, &block) - if @delegate.respond_to?(method) - @delegate.send(method, *args, &block) - else - super + # Forwards any missing method call to the \target. + def method_missing(method, *args, &block) + if @delegate.respond_to?(method) + @delegate.send(method, *args, &block) + else + super + end end - end end end end diff --git a/activerecord/lib/active_record/migration/compatibility.rb b/activerecord/lib/active_record/migration/compatibility.rb new file mode 100644 index 0000000000..2904634eb7 --- /dev/null +++ b/activerecord/lib/active_record/migration/compatibility.rb @@ -0,0 +1,126 @@ +module ActiveRecord + class Migration + module Compatibility # :nodoc: all + def self.find(version) + version = version.to_s + name = "V#{version.tr('.', '_')}" + unless const_defined?(name) + versions = constants.grep(/\AV[0-9_]+\z/).map { |s| s.to_s.delete("V").tr("_", ".").inspect } + raise ArgumentError, "Unknown migration version #{version.inspect}; expected one of #{versions.sort.join(', ')}" + end + const_get(name) + end + + V5_1 = Current + + class V5_0 < V5_1 + def create_table(table_name, options = {}) + if adapter_name == "PostgreSQL" + if options[:id] == :uuid && !options[:default] + options[:default] = "uuid_generate_v4()" + end + end + + # Since 5.1 Postgres adapter uses bigserial type for primary + # keys by default and MySQL uses bigint. This compat layer makes old migrations utilize + # serial/int type instead -- the way it used to work before 5.1. + if options[:id].blank? + options[:id] = :integer + options[:auto_increment] = true + end + + super + end + end + + class V4_2 < V5_0 + module TableDefinition + def references(*, **options) + options[:index] ||= false + super + end + alias :belongs_to :references + + def timestamps(**options) + options[:null] = true if options[:null].nil? + super + end + end + + def create_table(table_name, options = {}) + if block_given? + super(table_name, options) do |t| + class << t + prepend TableDefinition + end + yield t + end + else + super + end + end + + def change_table(table_name, options = {}) + if block_given? + super(table_name, options) do |t| + class << t + prepend TableDefinition + end + yield t + end + else + super + end + end + + def add_reference(*, **options) + options[:index] ||= false + super + end + alias :add_belongs_to :add_reference + + def add_timestamps(_, **options) + options[:null] = true if options[:null].nil? + super + end + + def index_exists?(table_name, column_name, options = {}) + column_names = Array(column_name).map(&:to_s) + options[:name] = + if options[:name].present? + options[:name].to_s + else + index_name(table_name, column: column_names) + end + super + end + + def remove_index(table_name, options = {}) + options = { column: options } unless options.is_a?(Hash) + options[:name] = index_name_for_remove(table_name, options) + super(table_name, options) + end + + private + + def index_name_for_remove(table_name, options = {}) + index_name = index_name(table_name, options) + + unless index_name_exists?(table_name, index_name, true) + if options.is_a?(Hash) && options.has_key?(:name) + options_without_column = options.dup + options_without_column.delete :column + index_name_without_column = index_name(table_name, options_without_column) + + return index_name_without_column if index_name_exists?(table_name, index_name_without_column, false) + end + + raise ArgumentError, "Index name '#{index_name}' on table '#{table_name}' does not exist" + end + + index_name + end + end + end + end +end diff --git a/activerecord/lib/active_record/migration/join_table.rb b/activerecord/lib/active_record/migration/join_table.rb index 05569fadbd..89789f00ea 100644 --- a/activerecord/lib/active_record/migration/join_table.rb +++ b/activerecord/lib/active_record/migration/join_table.rb @@ -3,13 +3,13 @@ module ActiveRecord module JoinTable #:nodoc: private - def find_join_table_name(table_1, table_2, options = {}) - options.delete(:table_name) || join_table_name(table_1, table_2) - end + def find_join_table_name(table_1, table_2, options = {}) + options.delete(:table_name) || join_table_name(table_1, table_2) + end - def join_table_name(table_1, table_2) - ModelSchema.derive_join_table_name(table_1, table_2).to_sym - end + def join_table_name(table_1, table_2) + ModelSchema.derive_join_table_name(table_1, table_2).to_sym + end end end end diff --git a/activerecord/lib/active_record/model_schema.rb b/activerecord/lib/active_record/model_schema.rb index 5a6f42ba09..2a28c6bf6d 100644 --- a/activerecord/lib/active_record/model_schema.rb +++ b/activerecord/lib/active_record/model_schema.rb @@ -2,55 +2,154 @@ module ActiveRecord module ModelSchema extend ActiveSupport::Concern + ## + # :singleton-method: primary_key_prefix_type + # :call-seq: primary_key_prefix_type + # + # The prefix type that will be prepended to every primary key column name. + # The options are +:table_name+ and +:table_name_with_underscore+. If the first is specified, + # the Product class will look for "productid" instead of "id" as the primary column. If the + # latter is specified, the Product class will look for "product_id" instead of "id". Remember + # that this is a global setting for all Active Records. + + ## + # :singleton-method: primary_key_prefix_type= + # :call-seq: primary_key_prefix_type=(prefix_type) + # + # Sets the prefix type that will be prepended to every primary key column name. + # The options are +:table_name+ and +:table_name_with_underscore+. If the first is specified, + # the Product class will look for "productid" instead of "id" as the primary column. If the + # latter is specified, the Product class will look for "product_id" instead of "id". Remember + # that this is a global setting for all Active Records. + + ## + # :singleton-method: table_name_prefix + # :call-seq: table_name_prefix + # + # The prefix string to prepend to every table name. + + ## + # :singleton-method: table_name_prefix= + # :call-seq: table_name_prefix=(prefix) + # + # Sets the prefix string to prepend to every table name. So if set to "basecamp_", all table + # names will be named like "basecamp_projects", "basecamp_people", etc. This is a convenient + # way of creating a namespace for tables in a shared database. By default, the prefix is the + # empty string. + # + # If you are organising your models within modules you can add a prefix to the models within + # a namespace by defining a singleton method in the parent module called table_name_prefix which + # returns your chosen prefix. + + ## + # :singleton-method: table_name_suffix + # :call-seq: table_name_suffix + # + # The suffix string to append to every table name. + + ## + # :singleton-method: table_name_suffix= + # :call-seq: table_name_suffix=(suffix) + # + # 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. + # + # If you are organising your models within modules, you can add a suffix to the models within + # a namespace by defining a singleton method in the parent module called table_name_suffix which + # returns your chosen suffix. + + ## + # :singleton-method: schema_migrations_table_name + # :call-seq: schema_migrations_table_name + # + # The name of the schema migrations table. By default, the value is <tt>"schema_migrations"</tt>. + + ## + # :singleton-method: schema_migrations_table_name= + # :call-seq: schema_migrations_table_name=(table_name) + # + # Sets the name of the schema migrations table. + + ## + # :singleton-method: internal_metadata_table_name + # :call-seq: internal_metadata_table_name + # + # The name of the internal metadata table. By default, the value is <tt>"ar_internal_metadata"</tt>. + + ## + # :singleton-method: internal_metadata_table_name= + # :call-seq: internal_metadata_table_name=(table_name) + # + # Sets the name of the internal metadata table. + + ## + # :singleton-method: protected_environments + # :call-seq: protected_environments + # + # The array of names of environments where destructive actions should be prohibited. By default, + # the value is <tt>["production"]</tt>. + + ## + # :singleton-method: protected_environments= + # :call-seq: protected_environments=(environments) + # + # Sets an array of names of environments where destructive actions should be prohibited. + + ## + # :singleton-method: pluralize_table_names + # :call-seq: pluralize_table_names + # + # 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. + + ## + # :singleton-method: pluralize_table_names= + # :call-seq: pluralize_table_names=(value) + # + # Set 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. + + ## + # :singleton-method: ignored_columns + # :call-seq: ignored_columns + # + # The list of columns names the model should ignore. Ignored columns won't have attribute + # accessors defined, and won't be referenced in SQL queries. + + ## + # :singleton-method: ignored_columns= + # :call-seq: ignored_columns=(columns) + # + # Sets the columns names the model should ignore. Ignored columns won't have attribute + # accessors defined, and won't be referenced in SQL queries. + included do - ## - # :singleton-method: - # Accessor for the prefix type that will be prepended to every primary key column name. - # The options are :table_name and :table_name_with_underscore. If the first is specified, - # the Product class will look for "productid" instead of "id" as the primary column. If the - # 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. mattr_accessor :primary_key_prefix_type, instance_writer: false - ## - # :singleton-method: - # Accessor for the name of the prefix string to prepend to every table name. So if set - # to "basecamp_", all table names will be named like "basecamp_projects", "basecamp_people", - # etc. This is a convenient way of creating a namespace for tables in a shared database. - # By default, the prefix is the empty string. - # - # If you are organising your models within modules you can add a prefix to the models within - # a namespace by defining a singleton method in the parent module called table_name_prefix which - # returns your chosen 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. - # - # If you are organising your models within modules, you can add a suffix to the models within - # a namespace by defining a singleton method in the parent module called table_name_suffix which - # returns your chosen suffix. class_attribute :table_name_suffix, instance_writer: false self.table_name_suffix = "" - ## - # :singleton-method: - # Accessor for the name of the schema migrations table. By default, the value is "schema_migrations" class_attribute :schema_migrations_table_name, instance_accessor: false self.schema_migrations_table_name = "schema_migrations" - ## - # :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. + class_attribute :internal_metadata_table_name, instance_accessor: false + self.internal_metadata_table_name = "ar_internal_metadata" + + class_attribute :protected_environments, instance_accessor: false + self.protected_environments = ["production"] + class_attribute :pluralize_table_names, instance_writer: false self.pluralize_table_names = true - self.inheritance_column = 'type' + class_attribute :ignored_columns, instance_accessor: false + self.ignored_columns = [].freeze + + self.inheritance_column = "type" delegate :type_for_attribute, to: :class end @@ -153,11 +252,11 @@ module ActiveRecord end def full_table_name_prefix #:nodoc: - (parents.detect{ |p| p.respond_to?(:table_name_prefix) } || self).table_name_prefix + (parents.detect { |p| p.respond_to?(:table_name_prefix) } || self).table_name_prefix end def full_table_name_suffix #:nodoc: - (parents.detect {|p| p.respond_to?(:table_name_suffix) } || self).table_name_suffix + (parents.detect { |p| p.respond_to?(:table_name_suffix) } || self).table_name_suffix end # Defines the name of the table column which will store the class name on single-table @@ -193,7 +292,7 @@ module ActiveRecord end # Sets the name of the sequence to use when generating ids to the given - # value, or (if the value is nil or false) to the value returned by the + # value, or (if the value is +nil+ or +false+) to the value returned by the # given block. This is required for Oracle and is useful for any # database which relies on sequences for primary key generation. # @@ -211,13 +310,29 @@ module ActiveRecord @explicit_sequence_name = true end + # Determines if the primary key values should be selected from their + # corresponding sequence before the insert statement. + def prefetch_primary_key? + connection.prefetch_primary_key?(table_name) + end + + # Returns the next value that will be used as the primary key on + # an insert statement. + def next_sequence_value + connection.next_sequence_value(sequence_name) + end + # Indicates whether the table associated with this class exists def table_exists? - connection.schema_cache.table_exists?(table_name) + connection.schema_cache.data_source_exists?(table_name) end def attributes_builder # :nodoc: - @attributes_builder ||= AttributeSet::Builder.new(attribute_types, primary_key) + @attributes_builder ||= AttributeSet::Builder.new(attribute_types, primary_key) do |name| + unless columns_hash.key?(name) + _default_attributes[name].dup + end + end end def columns_hash # :nodoc: @@ -232,15 +347,34 @@ module ActiveRecord def attribute_types # :nodoc: load_schema - @attribute_types ||= Hash.new(Type::Value.new) + @attribute_types ||= Hash.new(Type.default_value) end - def type_for_attribute(attr_name) # :nodoc: - attribute_types[attr_name] + def yaml_encoder # :nodoc: + @yaml_encoder ||= AttributeSet::YAMLEncoder.new(attribute_types) + end + + # Returns the type of the attribute with the given name, after applying + # all modifiers. This method is the only valid source of information for + # anything related to the types of a model's attributes. This method will + # access the database and load the model's schema if it is required. + # + # The return value of this method will implement the interface described + # by ActiveModel::Type::Value (though the object itself may not subclass + # it). + # + # +attr_name+ The name of the attribute to retrieve the type for. Must be + # a string + def type_for_attribute(attr_name, &block) + if block + attribute_types.fetch(attr_name, &block) + else + attribute_types[attr_name] + end end # Returns a hash where the keys are column names and the values are - # default values when instantiating the AR object for this table. + # default values when instantiating the Active Record object for this table. def column_defaults load_schema _default_attributes.to_hash @@ -258,7 +392,12 @@ module ActiveRecord # Returns an array of column objects where the primary id, all columns ending in "_id" or "_count", # and columns used for single table inheritance have been removed. def content_columns - @content_columns ||= columns.reject { |c| c.name == primary_key || c.name =~ /(_id|_count)$/ || c.name == inheritance_column } + @content_columns ||= columns.reject do |c| + c.name == primary_key || + c.name == inheritance_column || + c.name.end_with?("_id") || + c.name.end_with?("_count") + end end # Resets all the cached information about columns, which will cause them @@ -268,7 +407,7 @@ module ActiveRecord # when just after creating a table you want to populate it with some default # values, eg: # - # class CreateJobLevels < ActiveRecord::Migration + # class CreateJobLevels < ActiveRecord::Migration[5.0] # def up # create_table :job_levels do |t| # t.integer :id @@ -290,95 +429,76 @@ module ActiveRecord def reset_column_information connection.clear_cache! undefine_attribute_methods - connection.schema_cache.clear_table_cache!(table_name) + connection.schema_cache.clear_data_source_cache!(table_name) reload_schema_from_cache end private - def schema_loaded? - defined?(@columns_hash) && @columns_hash - end - - def load_schema - unless schema_loaded? - load_schema! + def schema_loaded? + defined?(@columns_hash) && @columns_hash end - end - def load_schema! - @columns_hash = connection.schema_cache.columns_hash(table_name) - @columns_hash.each do |name, column| - warn_if_deprecated_type(column) - define_attribute( - name, - connection.lookup_cast_type_from_column(column), - default: column.default, - user_provided_default: false - ) + def load_schema + unless schema_loaded? + load_schema! + end end - end - def reload_schema_from_cache - @arel_engine = nil - @arel_table = nil - @column_names = nil - @attribute_types = nil - @content_columns = nil - @default_attributes = nil - @inheritance_column = nil unless defined?(@explicit_inheritance_column) && @explicit_inheritance_column - @attributes_builder = nil - @columns = nil - @columns_hash = nil - @attribute_names = nil - end - - # Guesses the table name, but does not decorate it with prefix and suffix information. - def undecorated_table_name(class_name = base_class.name) - table_name = class_name.to_s.demodulize.underscore - pluralize_table_names ? table_name.pluralize : table_name - end + def load_schema! + @columns_hash = connection.schema_cache.columns_hash(table_name).except(*ignored_columns) + @columns_hash.each do |name, column| + define_attribute( + name, + connection.lookup_cast_type_from_column(column), + default: column.default, + user_provided_default: false + ) + end + end - # Computes and returns a table name according to default conventions. - def compute_table_name - base = base_class - if self == base - # Nested classes are prefixed with singular parent table name. - if parent < Base && !parent.abstract_class? - contained = parent.table_name - contained = contained.singularize if parent.pluralize_table_names - contained += '_' + def reload_schema_from_cache + @arel_engine = nil + @arel_table = nil + @column_names = nil + @attribute_types = nil + @content_columns = nil + @default_attributes = nil + @inheritance_column = nil unless defined?(@explicit_inheritance_column) && @explicit_inheritance_column + @attributes_builder = nil + @columns = nil + @columns_hash = nil + @attribute_names = nil + @yaml_encoder = nil + direct_descendants.each do |descendant| + descendant.send(:reload_schema_from_cache) end + end - "#{full_table_name_prefix}#{contained}#{undecorated_table_name(name)}#{full_table_name_suffix}" - else - # STI subclasses always use their superclass' table. - base.table_name + # Guesses the table name, but does not decorate it with prefix and suffix information. + def undecorated_table_name(class_name = base_class.name) + table_name = class_name.to_s.demodulize.underscore + pluralize_table_names ? table_name.pluralize : table_name end - end - def warn_if_deprecated_type(column) - return if attributes_to_define_after_schema_loads.key?(column.name) - if column.respond_to?(:oid) && column.sql_type.start_with?("point") - if column.array? - array_arguments = ", array: true" + # Computes and returns a table name according to default conventions. + def compute_table_name + base = base_class + if self == base + # Nested classes are prefixed with singular parent table name. + if parent < Base && !parent.abstract_class? + contained = parent.table_name + contained = contained.singularize if parent.pluralize_table_names + contained += "_" + end + + "#{full_table_name_prefix}#{contained}#{undecorated_table_name(name)}#{full_table_name_suffix}" else - array_arguments = "" + # STI subclasses always use their superclass' table. + base.table_name end - ActiveSupport::Deprecation.warn(<<-WARNING.strip_heredoc) - The behavior of the `:point` type will be changing in Rails 5.1 to - return a `Point` object, rather than an `Array`. If you'd like to - keep the old behavior, you can add this line to #{self.name}: - - attribute :#{column.name}, :legacy_point#{array_arguments} - - If you'd like the new behavior today, you can add this line: - - attribute :#{column.name}, :rails_5_1_point#{array_arguments} - WARNING end - end end end end diff --git a/activerecord/lib/active_record/nested_attributes.rb b/activerecord/lib/active_record/nested_attributes.rb index c942d0e265..e983026961 100644 --- a/activerecord/lib/active_record/nested_attributes.rb +++ b/activerecord/lib/active_record/nested_attributes.rb @@ -1,6 +1,6 @@ -require 'active_support/core_ext/hash/except' -require 'active_support/core_ext/object/try' -require 'active_support/core_ext/hash/indifferent_access' +require "active_support/core_ext/hash/except" +require "active_support/core_ext/object/try" +require "active_support/core_ext/hash/indifferent_access" module ActiveRecord module NestedAttributes #:nodoc: @@ -195,19 +195,27 @@ module ActiveRecord # Nested attributes for an associated collection can also be passed in # the form of a hash of hashes instead of an array of hashes: # - # Member.create(name: 'joe', - # posts_attributes: { first: { title: 'Foo' }, - # second: { title: 'Bar' } }) + # Member.create( + # name: 'joe', + # posts_attributes: { + # first: { title: 'Foo' }, + # second: { title: 'Bar' } + # } + # ) # # has the same effect as # - # Member.create(name: 'joe', - # posts_attributes: [ { title: 'Foo' }, - # { title: 'Bar' } ]) + # Member.create( + # name: 'joe', + # posts_attributes: [ + # { title: 'Foo' }, + # { title: 'Bar' } + # ] + # ) # # The keys of the hash which is the value for +:posts_attributes+ are # ignored in this case. - # However, it is not allowed to use +'id'+ or +:id+ for one of + # However, it is not allowed to use <tt>'id'</tt> or <tt>:id</tt> for one of # such keys, otherwise the hash will be wrapped in an array and # interpreted as an attribute hash for a single post. # @@ -259,7 +267,7 @@ module ActiveRecord # member.avatar_attributes = {icon: 'sad'} # member.avatar.width # => 200 module ClassMethods - REJECT_ALL_BLANK_PROC = proc { |attributes| attributes.all? { |key, value| key == '_destroy' || value.blank? } } + REJECT_ALL_BLANK_PROC = proc { |attributes| attributes.all? { |key, value| key == "_destroy" || value.blank? } } # Defines an attributes writer for the specified association(s). # @@ -309,7 +317,7 @@ module ActiveRecord # # creates avatar_attributes= and posts_attributes= # accepts_nested_attributes_for :avatar, :posts, allow_destroy: true def accepts_nested_attributes_for(*attr_names) - options = { :allow_destroy => false, :update_only => false } + options = { allow_destroy: false, update_only: false } options.update(attr_names.extract_options!) options.assert_valid_keys(:allow_destroy, :reject_if, :limit, :update_only) options[:reject_if] = REJECT_ALL_BLANK_PROC if options[:reject_if] == :all_blank @@ -333,27 +341,27 @@ module ActiveRecord private - # Generates a writer method for this association. Serves as a point for - # accessing the objects in the association. For example, this method - # could generate the following: - # - # def pirate_attributes=(attributes) - # assign_nested_attributes_for_one_to_one_association(:pirate, attributes) - # end - # - # This redirects the attempts to write objects in an association through - # the helper methods defined below. Makes it seem like the nested - # associations are just regular associations. - def generate_association_writer(association_name, type) - generated_association_methods.module_eval <<-eoruby, __FILE__, __LINE__ + 1 - if method_defined?(:#{association_name}_attributes=) - remove_method(:#{association_name}_attributes=) - end - def #{association_name}_attributes=(attributes) - assign_nested_attributes_for_#{type}_association(:#{association_name}, attributes) - end - eoruby - end + # Generates a writer method for this association. Serves as a point for + # accessing the objects in the association. For example, this method + # could generate the following: + # + # def pirate_attributes=(attributes) + # assign_nested_attributes_for_one_to_one_association(:pirate, attributes) + # end + # + # This redirects the attempts to write objects in an association through + # the helper methods defined below. Makes it seem like the nested + # associations are just regular associations. + def generate_association_writer(association_name, type) + generated_association_methods.module_eval <<-eoruby, __FILE__, __LINE__ + 1 + if method_defined?(:#{association_name}_attributes=) + remove_method(:#{association_name}_attributes=) + end + def #{association_name}_attributes=(attributes) + assign_nested_attributes_for_#{type}_association(:#{association_name}, attributes) + end + eoruby + end end # Returns ActiveRecord::AutosaveAssociation::marked_for_destruction? It's @@ -367,192 +375,214 @@ module ActiveRecord private - # Attribute hash keys that should not be assigned as normal attributes. - # These hash keys are nested attributes implementation details. - UNASSIGNABLE_KEYS = %w( id _destroy ) - - # Assigns the given attributes to the association. - # - # If an associated record does not yet exist, one will be instantiated. If - # an associated record already exists, the method's behavior depends on - # the value of the update_only option. If update_only is +false+ and the - # given attributes include an <tt>:id</tt> that matches the existing record's - # id, then the existing record will be modified. If no <tt>:id</tt> is provided - # it will be replaced with a new record. If update_only is +true+ the existing - # record will be modified regardless of whether an <tt>:id</tt> is provided. - # - # If the given attributes include a matching <tt>:id</tt> attribute, or - # update_only is true, and a <tt>:_destroy</tt> key set to a truthy value, - # then the existing record will be marked for destruction. - def assign_nested_attributes_for_one_to_one_association(association_name, attributes) - options = self.nested_attributes_options[association_name] - attributes = attributes.with_indifferent_access - existing_record = send(association_name) - - if (options[:update_only] || !attributes['id'].blank?) && existing_record && - (options[:update_only] || existing_record.id.to_s == attributes['id'].to_s) - assign_to_or_mark_for_destruction(existing_record, attributes, options[:allow_destroy]) unless call_reject_if(association_name, attributes) - - elsif attributes['id'].present? - raise_nested_attributes_record_not_found!(association_name, attributes['id']) - - elsif !reject_new_record?(association_name, attributes) - assignable_attributes = attributes.except(*UNASSIGNABLE_KEYS) - - if existing_record && existing_record.new_record? - existing_record.assign_attributes(assignable_attributes) - association(association_name).initialize_attributes(existing_record) - else - method = "build_#{association_name}" - if respond_to?(method) - send(method, assignable_attributes) + # Attribute hash keys that should not be assigned as normal attributes. + # These hash keys are nested attributes implementation details. + UNASSIGNABLE_KEYS = %w( id _destroy ) + + # Assigns the given attributes to the association. + # + # If an associated record does not yet exist, one will be instantiated. If + # an associated record already exists, the method's behavior depends on + # the value of the update_only option. If update_only is +false+ and the + # given attributes include an <tt>:id</tt> that matches the existing record's + # id, then the existing record will be modified. If no <tt>:id</tt> is provided + # it will be replaced with a new record. If update_only is +true+ the existing + # record will be modified regardless of whether an <tt>:id</tt> is provided. + # + # If the given attributes include a matching <tt>:id</tt> attribute, or + # update_only is true, and a <tt>:_destroy</tt> key set to a truthy value, + # then the existing record will be marked for destruction. + def assign_nested_attributes_for_one_to_one_association(association_name, attributes) + options = self.nested_attributes_options[association_name] + if attributes.respond_to?(:permitted?) + attributes = attributes.to_h + end + attributes = attributes.with_indifferent_access + existing_record = send(association_name) + + if (options[:update_only] || !attributes["id"].blank?) && existing_record && + (options[:update_only] || existing_record.id.to_s == attributes["id"].to_s) + assign_to_or_mark_for_destruction(existing_record, attributes, options[:allow_destroy]) unless call_reject_if(association_name, attributes) + + elsif attributes["id"].present? + raise_nested_attributes_record_not_found!(association_name, attributes["id"]) + + elsif !reject_new_record?(association_name, attributes) + assignable_attributes = attributes.except(*UNASSIGNABLE_KEYS) + + if existing_record && existing_record.new_record? + existing_record.assign_attributes(assignable_attributes) + association(association_name).initialize_attributes(existing_record) else - raise ArgumentError, "Cannot build association `#{association_name}'. Are you trying to build a polymorphic one-to-one association?" + method = "build_#{association_name}" + if respond_to?(method) + send(method, assignable_attributes) + else + raise ArgumentError, "Cannot build association `#{association_name}'. Are you trying to build a polymorphic one-to-one association?" + end end end end - end - # Assigns the given attributes to the collection association. - # - # Hashes with an <tt>:id</tt> value matching an existing associated record - # will update that record. Hashes without an <tt>:id</tt> value will build - # a new record for the association. Hashes with a matching <tt>:id</tt> - # value and a <tt>:_destroy</tt> key set to a truthy value will mark the - # matched record for destruction. - # - # For example: - # - # assign_nested_attributes_for_collection_association(:people, { - # '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 - # person with the name 'John', and mark the associated Person with ID 2 - # for destruction. - # - # Also accepts an Array of attribute hashes: - # - # assign_nested_attributes_for_collection_association(:people, [ - # { 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] + # Assigns the given attributes to the collection association. + # + # Hashes with an <tt>:id</tt> value matching an existing associated record + # will update that record. Hashes without an <tt>:id</tt> value will build + # a new record for the association. Hashes with a matching <tt>:id</tt> + # value and a <tt>:_destroy</tt> key set to a truthy value will mark the + # matched record for destruction. + # + # For example: + # + # assign_nested_attributes_for_collection_association(:people, { + # '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 + # person with the name 'John', and mark the associated Person with ID 2 + # for destruction. + # + # Also accepts an Array of attribute hashes: + # + # assign_nested_attributes_for_collection_association(:people, [ + # { 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] + if attributes_collection.respond_to?(:permitted?) + attributes_collection = attributes_collection.to_h + end - unless attributes_collection.is_a?(Hash) || attributes_collection.is_a?(Array) - raise ArgumentError, "Hash or Array expected, got #{attributes_collection.class.name} (#{attributes_collection.inspect})" - end + unless attributes_collection.is_a?(Hash) || attributes_collection.is_a?(Array) + raise ArgumentError, "Hash or Array expected, got #{attributes_collection.class.name} (#{attributes_collection.inspect})" + end - check_record_limit!(options[:limit], attributes_collection) + check_record_limit!(options[:limit], attributes_collection) - if attributes_collection.is_a? Hash - keys = attributes_collection.keys - attributes_collection = if keys.include?('id') || keys.include?(:id) - [attributes_collection] - else - attributes_collection.values + if attributes_collection.is_a? Hash + keys = attributes_collection.keys + attributes_collection = if keys.include?("id") || keys.include?(:id) + [attributes_collection] + else + attributes_collection.values + end end - end - association = association(association_name) + association = association(association_name) - existing_records = if association.loaded? - association.target - else - attribute_ids = attributes_collection.map {|a| a['id'] || a[:id] }.compact - attribute_ids.empty? ? [] : association.scope.where(association.klass.primary_key => attribute_ids) - end + existing_records = if association.loaded? + association.target + else + attribute_ids = attributes_collection.map { |a| a["id"] || a[:id] }.compact + attribute_ids.empty? ? [] : association.scope.where(association.klass.primary_key => attribute_ids) + end - attributes_collection.each do |attributes| - attributes = attributes.with_indifferent_access + attributes_collection.each do |attributes| + if attributes.respond_to?(:permitted?) + attributes = attributes.to_h + end + attributes = attributes.with_indifferent_access - if attributes['id'].blank? - unless reject_new_record?(association_name, attributes) - association.build(attributes.except(*UNASSIGNABLE_KEYS)) + if attributes["id"].blank? + unless reject_new_record?(association_name, attributes) + association.build(attributes.except(*UNASSIGNABLE_KEYS)) + end + elsif existing_record = existing_records.detect { |record| record.id.to_s == attributes["id"].to_s } + unless call_reject_if(association_name, attributes) + # Make sure we are operating on the actual object which is in the association's + # proxy_target array (either by finding it, or adding it if not found) + # Take into account that the proxy_target may have changed due to callbacks + target_record = association.target.detect { |record| record.id.to_s == attributes["id"].to_s } + if target_record + existing_record = target_record + else + association.add_to_target(existing_record, :skip_callbacks) + end + + assign_to_or_mark_for_destruction(existing_record, attributes, options[:allow_destroy]) + end + else + raise_nested_attributes_record_not_found!(association_name, attributes["id"]) end - elsif existing_record = existing_records.detect { |record| record.id.to_s == attributes['id'].to_s } - unless call_reject_if(association_name, attributes) - # Make sure we are operating on the actual object which is in the association's - # proxy_target array (either by finding it, or adding it if not found) - # Take into account that the proxy_target may have changed due to callbacks - target_record = association.target.detect { |record| record.id.to_s == attributes['id'].to_s } - if target_record - existing_record = target_record + end + end + + # Takes in a limit and checks if the attributes_collection has too many + # records. It accepts limit in the form of symbol, proc, or + # number-like object (anything that can be compared with an integer). + # + # Raises TooManyRecords error if the attributes_collection is + # larger than the limit. + def check_record_limit!(limit, attributes_collection) + if limit + limit = \ + case limit + when Symbol + send(limit) + when Proc + limit.call else - association.add_to_target(existing_record, :skip_callbacks) + limit end - assign_to_or_mark_for_destruction(existing_record, attributes, options[:allow_destroy]) + if limit && attributes_collection.size > limit + raise TooManyRecords, "Maximum #{limit} records are allowed. Got #{attributes_collection.size} records instead." end - else - raise_nested_attributes_record_not_found!(association_name, attributes['id']) end end - end - # Takes in a limit and checks if the attributes_collection has too many - # records. It accepts limit in the form of symbol, proc, or - # number-like object (anything that can be compared with an integer). - # - # Raises TooManyRecords error if the attributes_collection is - # larger than the limit. - def check_record_limit!(limit, attributes_collection) - if limit - limit = case limit - when Symbol - send(limit) - when Proc - limit.call - else - limit - end + # Updates a record with the +attributes+ or marks it for destruction if + # +allow_destroy+ is +true+ and has_destroy_flag? returns +true+. + def assign_to_or_mark_for_destruction(record, attributes, allow_destroy) + record.assign_attributes(attributes.except(*UNASSIGNABLE_KEYS)) + record.mark_for_destruction if has_destroy_flag?(attributes) && allow_destroy + end - if limit && attributes_collection.size > limit - raise TooManyRecords, "Maximum #{limit} records are allowed. Got #{attributes_collection.size} records instead." - end + # Determines if a hash contains a truthy _destroy key. + def has_destroy_flag?(hash) + Type::Boolean.new.cast(hash["_destroy"]) end - end - # Updates a record with the +attributes+ or marks it for destruction if - # +allow_destroy+ is +true+ and has_destroy_flag? returns +true+. - def assign_to_or_mark_for_destruction(record, attributes, allow_destroy) - record.assign_attributes(attributes.except(*UNASSIGNABLE_KEYS)) - record.mark_for_destruction if has_destroy_flag?(attributes) && allow_destroy - end + # Determines if a new record should be rejected by checking + # has_destroy_flag? or if a <tt>:reject_if</tt> proc exists for this + # association and evaluates to +true+. + def reject_new_record?(association_name, attributes) + will_be_destroyed?(association_name, attributes) || call_reject_if(association_name, attributes) + end - # Determines if a hash contains a truthy _destroy key. - def has_destroy_flag?(hash) - Type::Boolean.new.cast(hash['_destroy']) - end + # Determines if a record with the particular +attributes+ should be + # rejected by calling the reject_if Symbol or Proc (if defined). + # The reject_if option is defined by +accepts_nested_attributes_for+. + # + # Returns false if there is a +destroy_flag+ on the attributes. + def call_reject_if(association_name, attributes) + return false if will_be_destroyed?(association_name, attributes) - # Determines if a new record should be rejected by checking - # has_destroy_flag? or if a <tt>:reject_if</tt> proc exists for this - # association and evaluates to +true+. - def reject_new_record?(association_name, attributes) - has_destroy_flag?(attributes) || call_reject_if(association_name, attributes) - end + case callback = self.nested_attributes_options[association_name][:reject_if] + when Symbol + method(callback).arity == 0 ? send(callback) : send(callback, attributes) + when Proc + callback.call(attributes) + end + end - # Determines if a record with the particular +attributes+ should be - # rejected by calling the reject_if Symbol or Proc (if defined). - # The reject_if option is defined by +accepts_nested_attributes_for+. - # - # Returns false if there is a +destroy_flag+ on the attributes. - def call_reject_if(association_name, attributes) - return false if has_destroy_flag?(attributes) - case callback = self.nested_attributes_options[association_name][:reject_if] - when Symbol - method(callback).arity == 0 ? send(callback) : send(callback, attributes) - when Proc - callback.call(attributes) + # Only take into account the destroy flag if <tt>:allow_destroy</tt> is true + def will_be_destroyed?(association_name, attributes) + allow_destroy?(association_name) && has_destroy_flag?(attributes) end - end - def raise_nested_attributes_record_not_found!(association_name, record_id) - raise RecordNotFound, "Couldn't find #{self.class._reflect_on_association(association_name).klass.name} with ID=#{record_id} for #{self.class.name} with ID=#{id}" - end + def allow_destroy?(association_name) + nested_attributes_options[association_name][:allow_destroy] + end + + def raise_nested_attributes_record_not_found!(association_name, record_id) + model = self.class._reflect_on_association(association_name).klass.name + raise RecordNotFound.new("Couldn't find #{model} with ID=#{record_id} for #{self.class.name} with ID=#{id}", + model, "id", record_id) + end end end diff --git a/activerecord/lib/active_record/no_touching.rb b/activerecord/lib/active_record/no_touching.rb index edb5066fa0..4059020e25 100644 --- a/activerecord/lib/active_record/no_touching.rb +++ b/activerecord/lib/active_record/no_touching.rb @@ -45,6 +45,10 @@ module ActiveRecord NoTouching.applied_to?(self.class) end + def touch_later(*) # :nodoc: + super unless no_touching? + end + def touch(*) # :nodoc: super unless no_touching? end diff --git a/activerecord/lib/active_record/null_relation.rb b/activerecord/lib/active_record/null_relation.rb index 74894d0c37..2bb7ed6d5e 100644 --- a/activerecord/lib/active_record/null_relation.rb +++ b/activerecord/lib/active_record/null_relation.rb @@ -1,11 +1,5 @@ -# -*- coding: utf-8 -*- - module ActiveRecord module NullRelation # :nodoc: - def exec_queries - @records = [] - end - def pluck(*column_names) [] end @@ -22,10 +16,6 @@ module ActiveRecord 0 end - def size - calculate :size, nil - end - def empty? true end @@ -50,33 +40,12 @@ module ActiveRecord "" end - def count(*) - calculate :count, nil - end - - def sum(*) - calculate :sum, nil - end - - def average(*) - calculate :average, nil - end - - def minimum(*) - calculate :minimum, nil - end - - def maximum(*) - calculate :maximum, nil - end - def calculate(operation, _column_name) - if [:count, :sum, :size].include? operation + case operation + when :count, :sum group_values.any? ? Hash.new : 0 - elsif [:average, :minimum, :maximum].include?(operation) && group_values.any? - Hash.new - else - nil + when :average, :minimum, :maximum + group_values.any? ? Hash.new : nil end end @@ -87,5 +56,11 @@ module ActiveRecord def or(other) other.spawn end + + private + + def exec_queries + @records = [].freeze + end end end diff --git a/activerecord/lib/active_record/persistence.rb b/activerecord/lib/active_record/persistence.rb index 0a6e4ac0bd..60d8e95b21 100644 --- a/activerecord/lib/active_record/persistence.rb +++ b/activerecord/lib/active_record/persistence.rb @@ -1,5 +1,5 @@ module ActiveRecord - # = Active Record Persistence + # = Active Record \Persistence module Persistence extend ActiveSupport::Concern @@ -61,12 +61,12 @@ module ActiveRecord # +instantiate+ instead of +new+, finder methods ensure they get new # instances of the appropriate class for each record. # - # See +ActiveRecord::Inheritance#discriminate_class_for_record+ to see + # See <tt>ActiveRecord::Inheritance#discriminate_class_for_record</tt> to see # how this "single-table" inheritance mapping is implemented. - def instantiate(attributes, column_types = {}) + def instantiate(attributes, column_types = {}, &block) klass = discriminate_class_for_record(attributes) attributes = klass.attributes_builder.build_from_database(attributes, column_types) - klass.allocate.init_with('attributes' => attributes, 'new_record' => false) + klass.allocate.init_with("attributes" => attributes, "new_record" => false, &block) end private @@ -102,12 +102,12 @@ module ActiveRecord # Saves the model. # - # If the model is new a record gets created in the database, otherwise + # If the model is new, a record gets created in the database, otherwise # the existing record gets updated. # - # By default, save always run validations. If any of them fail the action - # is cancelled and +save+ returns +false+. However, if you supply - # validate: false, validations are bypassed altogether. See + # By default, save always runs validations. If any of them fail the action + # is cancelled and #save returns +false+, and the record won't be saved. However, if you supply + # <tt>validate: false</tt>, validations are bypassed altogether. See # ActiveRecord::Validations for more information. # # By default, #save also sets the +updated_at+/+updated_on+ attributes to @@ -132,9 +132,10 @@ module ActiveRecord # If the model is new, a record gets created in the database, otherwise # the existing record gets updated. # - # With <tt>save!</tt> validations always run. If any of them fail - # ActiveRecord::RecordInvalid gets raised. See ActiveRecord::Validations - # for more information. + # By default, #save! always runs validations. If any of them fail + # ActiveRecord::RecordInvalid gets raised, and the record won't be saved. However, if you supply + # <tt>validate: false</tt>, validations are bypassed altogether. See + # ActiveRecord::Validations for more information. # # By default, #save! also sets the +updated_at+/+updated_on+ attributes to # the current time. However, if you supply <tt>touch: false</tt>, these @@ -158,7 +159,7 @@ module ActiveRecord # The row is simply removed with an SQL +DELETE+ statement on the # record's primary key, and no callbacks are executed. # - # Note that this will also delete records marked as <tt>readonly?</tt>. + # Note that this will also delete records marked as {#readonly?}[rdoc-ref:Core#readonly?]. # # To enforce the object's +before_destroy+ and +after_destroy+ # callbacks or any <tt>:dependent</tt> association @@ -177,10 +178,14 @@ module ActiveRecord # and #destroy returns +false+. # See ActiveRecord::Callbacks for further details. def destroy - raise ReadOnlyRecord, "#{self.class} is marked as readonly" if readonly? + _raise_readonly_record_error if readonly? destroy_associations self.class.connection.add_transaction_record(self) - destroy_row if persisted? + @_trigger_destroy_callback = if persisted? + destroy_row > 0 + else + true + end @destroyed = true freeze end @@ -193,7 +198,7 @@ module ActiveRecord # and #destroy! raises ActiveRecord::RecordNotDestroyed. # See ActiveRecord::Callbacks for further details. def destroy! - destroy || raise(RecordNotDestroyed.new("Failed to destroy the record", self)) + destroy || _raise_record_not_destroyed end # Returns an instance of the specified +klass+ with the attributes of the @@ -207,18 +212,19 @@ module ActiveRecord # Note: The new instance will share a link to the same attributes as the original class. # Therefore the sti column value will still be the same. # Any change to the attributes on either instance will affect both instances. - # If you want to change the sti column as well, use +becomes!+ instead. + # If you want to change the sti column as well, use #becomes! instead. def becomes(klass) became = klass.new became.instance_variable_set("@attributes", @attributes) + became.instance_variable_set("@mutation_tracker", @mutation_tracker) if defined?(@mutation_tracker) became.instance_variable_set("@changed_attributes", attributes_changed_by_setter) became.instance_variable_set("@new_record", new_record?) became.instance_variable_set("@destroyed", destroyed?) - became.instance_variable_set("@errors", errors) + became.errors.copy!(errors) became end - # Wrapper around +becomes+ that also changes the instance's sti column value. + # Wrapper around #becomes that also changes the instance's sti column value. # This is especially useful if you want to persist the changed class in your # database. # @@ -238,19 +244,24 @@ module ActiveRecord # This is especially useful for boolean flags on existing records. Also note that # # * Validation is skipped. - # * Callbacks are invoked. + # * \Callbacks are invoked. # * updated_at/updated_on column is updated if that column is available. # * Updates all the attributes that are dirty in this object. # - # This method raises an +ActiveRecord::ActiveRecordError+ if the + # This method raises an ActiveRecord::ActiveRecordError if the # attribute is marked as readonly. # - # See also +update_column+. + # Also see #update_column. def update_attribute(name, value) name = name.to_s verify_readonly_attribute(name) public_send("#{name}=", value) - save(validate: false) if changed? + + if has_changes_to_save? + save(validate: false) + else + true + end end # Updates the attributes of the model from the passed-in hash and saves the @@ -267,8 +278,8 @@ module ActiveRecord alias update_attributes update - # Updates its receiver just like +update+ but calls <tt>save!</tt> instead - # of +save+, so an exception is raised if the record is invalid. + # Updates its receiver just like #update but calls #save! instead + # of +save+, so an exception is raised if the record is invalid and saving will fail. def update!(attributes) # The following transaction covers any possible database side-effects of the # attributes assignment. For example, setting the IDs of a child collection. @@ -294,11 +305,12 @@ module ActiveRecord # the database, but take into account that in consequence the regular update # procedures are totally bypassed. In particular: # - # * Validations are skipped. - # * Callbacks are skipped. + # * \Validations are skipped. + # * \Callbacks are skipped. # * +updated_at+/+updated_on+ are not updated. + # * However, attributes are serialized with the same rules as ActiveRecord::Relation#update_all # - # This method raises an +ActiveRecord::ActiveRecordError+ when called on new + # This method raises an ActiveRecord::ActiveRecordError when called on new # objects, or when at least one of the attributes is marked as readonly. def update_columns(attributes) raise ActiveRecordError, "cannot update a new record" if new_record? @@ -326,42 +338,52 @@ module ActiveRecord self end - # Wrapper around +increment+ that saves the record. This method differs from + # Wrapper around #increment that saves the record. This method differs from # its non-bang version in that it passes through the attribute setter. # Saving is not subjected to validation checks. Returns +true+ if the # record could be saved. def increment!(attribute, by = 1) - increment(attribute, by).update_attribute(attribute, self[attribute]) + increment(attribute, by) + change = public_send(attribute) - (attribute_in_database(attribute.to_s) || 0) + self.class.update_counters(id, attribute => change) + clear_attribute_change(attribute) # eww + self end # Initializes +attribute+ to zero if +nil+ and subtracts the value passed as +by+ (default is 1). # The decrement is performed directly on the underlying attribute, no setter is invoked. # Only makes sense for number-based attributes. Returns +self+. def decrement(attribute, by = 1) - self[attribute] ||= 0 - self[attribute] -= by - self + increment(attribute, -by) end - # Wrapper around +decrement+ that saves the record. This method differs from - # its non-bang version in that it passes through the attribute setter. + # Wrapper around #decrement that saves the record. This method differs from + # its non-bang version in the sense that it passes through the attribute setter. # Saving is not subjected to validation checks. Returns +true+ if the # record could be saved. def decrement!(attribute, by = 1) - decrement(attribute, by).update_attribute(attribute, self[attribute]) + increment!(attribute, -by) end # Assigns to +attribute+ the boolean opposite of <tt>attribute?</tt>. So # if the predicate returns +true+ the attribute will become +false+. This # method toggles directly the underlying value without calling any setter. # Returns +self+. + # + # Example: + # + # user = User.first + # user.banned? # => false + # user.toggle(:banned) + # user.banned? # => true + # def toggle(attribute) self[attribute] = !public_send("#{attribute}?") self end - # Wrapper around +toggle+ that saves the record. This method differs from - # its non-bang version in that it passes through the attribute setter. + # Wrapper around #toggle that saves the record. This method differs from + # its non-bang version in the sense that it passes through the attribute setter. # Saving is not subjected to validation checks. Returns +true+ if the # record could be saved. def toggle!(attribute) @@ -383,7 +405,7 @@ module ActiveRecord # Attributes are reloaded from the database, and caches busted, in # particular the associations cache and the QueryCache. # - # If the record no longer exists in the database <tt>ActiveRecord::RecordNotFound</tt> + # If the record no longer exists in the database ActiveRecord::RecordNotFound # is raised. Otherwise, in addition to the in-place modification the method # returns +self+ for convenience. # @@ -426,7 +448,7 @@ module ActiveRecord self.class.unscoped { self.class.find(id) } end - @attributes = fresh_object.instance_variable_get('@attributes') + @attributes = fresh_object.instance_variable_get("@attributes") @new_record = false self end @@ -445,8 +467,8 @@ module ActiveRecord # product.touch(:designed_at) # updates the designed_at attribute and updated_at/on # product.touch(:started_at, :ended_at) # updates started_at, ended_at and updated_at/on attributes # - # If used along with +belongs_to+ then +touch+ will invoke +touch+ method on - # associated object. + # If used along with {belongs_to}[rdoc-ref:Associations::ClassMethods#belongs_to] + # then +touch+ will invoke +touch+ method on associated object. # # class Brake < ActiveRecord::Base # belongs_to :car, touch: true @@ -466,7 +488,12 @@ module ActiveRecord # ball.touch(:updated_at) # => raises ActiveRecordError # def touch(*names, time: nil) - raise ActiveRecordError, "cannot touch on a new record object" unless persisted? + unless persisted? + raise ActiveRecordError, <<-MSG.squish + cannot touch on a new or destroyed record object. Consider using + persisted?, new_record?, or destroyed? before touching + MSG + end time ||= current_time_from_proper_timezone attributes = timestamp_attributes_for_update_in_model @@ -480,7 +507,6 @@ module ActiveRecord changes[column] = write_attribute(column, time) end - clear_attribute_changes(changes.keys) primary_key = self.class.primary_key scope = self.class.unscoped.where(primary_key => _read_attribute(primary_key)) @@ -490,12 +516,14 @@ module ActiveRecord changes[locking_column] = increment_lock end + clear_attribute_changes(changes.keys) result = scope.update_all(changes) == 1 if !result && locking_enabled? raise ActiveRecord::StaleObjectError.new(self, "touch") end + @_trigger_update_callback = result result else true @@ -517,7 +545,7 @@ module ActiveRecord end def create_or_update(*args) - raise ReadOnlyRecord, "#{self.class} is marked as readonly" if readonly? + _raise_readonly_record_error if readonly? result = new_record? ? _create_record : _update_record(*args) result != false end @@ -527,10 +555,13 @@ module ActiveRecord def _update_record(attribute_names = self.attribute_names) attributes_values = arel_attributes_with_values_for_update(attribute_names) if attributes_values.empty? - 0 + rows_affected = 0 + @_trigger_update_callback = true else - self.class.unscoped._update_record attributes_values, id, id_was + rows_affected = self.class.unscoped._update_record attributes_values, id, id_in_database + @_trigger_update_callback = rows_affected > 0 end + rows_affected end # Creates a record with values matching those of the instance attributes @@ -548,5 +579,20 @@ module ActiveRecord def verify_readonly_attribute(name) raise ActiveRecordError, "#{name} is marked as readonly" if self.class.readonly_attributes.include?(name) end + + def _raise_record_not_destroyed + @_association_destroy_exception ||= nil + raise @_association_destroy_exception || RecordNotDestroyed.new("Failed to destroy the record", self) + ensure + @_association_destroy_exception = nil + end + + def belongs_to_touch_method + :touch + end + + def _raise_readonly_record_error + raise ReadOnlyRecord, "#{self.class} is marked as readonly" + end end end diff --git a/activerecord/lib/active_record/query_cache.rb b/activerecord/lib/active_record/query_cache.rb index dcb2bd3d84..ec246e97bc 100644 --- a/activerecord/lib/active_record/query_cache.rb +++ b/activerecord/lib/active_record/query_cache.rb @@ -5,7 +5,7 @@ module ActiveRecord # Enable the query cache within the block if Active Record is configured. # If it's not, it will execute the given block. def cache(&block) - if ActiveRecord::Base.connected? + if connected? connection.cache(&block) else yield @@ -15,7 +15,7 @@ module ActiveRecord # Disable the query cache within the block if Active Record is configured. # If it's not, it will execute the given block. def uncached(&block) - if ActiveRecord::Base.connected? + if connected? connection.uncached(&block) else yield @@ -23,34 +23,25 @@ module ActiveRecord end end - def initialize(app) - @app = app - end - - def call(env) - connection = ActiveRecord::Base.connection - enabled = connection.query_cache_enabled - connection_id = ActiveRecord::Base.connection_id - connection.enable_query_cache! + def self.run + caching_pool = ActiveRecord::Base.connection_pool + caching_was_enabled = caching_pool.query_cache_enabled - response = @app.call(env) - response[2] = Rack::BodyProxy.new(response[2]) do - restore_query_cache_settings(connection_id, enabled) - end + caching_pool.enable_query_cache! - response - rescue Exception => e - restore_query_cache_settings(connection_id, enabled) - raise e + [caching_pool, caching_was_enabled] end - private + def self.complete((caching_pool, caching_was_enabled)) + caching_pool.disable_query_cache! unless caching_was_enabled - def restore_query_cache_settings(connection_id, enabled) - ActiveRecord::Base.connection_id = connection_id - ActiveRecord::Base.connection.clear_query_cache - ActiveRecord::Base.connection.disable_query_cache! unless enabled + ActiveRecord::Base.connection_handler.connection_pool_list.each do |pool| + pool.release_connection if pool.active_connection? && !pool.connection.transaction_open? + end end + def self.install_executor_hooks(executor = ActiveSupport::Executor) + executor.register_hook(self) + end end end diff --git a/activerecord/lib/active_record/querying.rb b/activerecord/lib/active_record/querying.rb index 4e597590e9..36689f6559 100644 --- a/activerecord/lib/active_record/querying.rb +++ b/activerecord/lib/active_record/querying.rb @@ -1,15 +1,15 @@ module ActiveRecord module Querying - delegate :find, :take, :take!, :first, :first!, :last, :last!, :exists?, :any?, :many?, to: :all - delegate :second, :second!, :third, :third!, :fourth, :fourth!, :fifth, :fifth!, :forty_two, :forty_two!, to: :all + delegate :find, :take, :take!, :first, :first!, :last, :last!, :exists?, :any?, :many?, :none?, :one?, to: :all + delegate :second, :second!, :third, :third!, :fourth, :fourth!, :fifth, :fifth!, :forty_two, :forty_two!, :third_to_last, :third_to_last!, :second_to_last, :second_to_last!, 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 - delegate :select, :group, :order, :except, :reorder, :limit, :offset, :joins, :or, + delegate :find_each, :find_in_batches, :in_batches, to: :all + delegate :select, :group, :order, :except, :reorder, :limit, :offset, :joins, :left_joins, :left_outer_joins, :or, :where, :rewhere, :preload, :eager_load, :includes, :from, :lock, :readonly, - :having, :create_with, :uniq, :distinct, :references, :none, :unscope, to: :all + :having, :create_with, :uniq, :distinct, :references, :none, :unscope, :merge, to: :all delegate :count, :average, :minimum, :maximum, :sum, :calculate, to: :all delegate :pluck, :ids, to: :all @@ -35,8 +35,8 @@ module ActiveRecord # # Post.find_by_sql ["SELECT title FROM posts WHERE author = ? AND created > ?", author_id, start_date] # Post.find_by_sql ["SELECT body FROM comments WHERE author = :user_id OR approved_by = :user_id", { :user_id => user_id }] - def find_by_sql(sql, binds = []) - result_set = connection.select_all(sanitize_sql(sql), "#{name} Load", binds) + def find_by_sql(sql, binds = [], preparable: nil, &block) + result_set = connection.select_all(sanitize_sql(sql), "#{name} Load", binds, preparable: preparable) column_types = result_set.column_types.dup columns_hash.each_key { |k| column_types.delete k } message_bus = ActiveSupport::Notifications.instrumenter @@ -46,8 +46,8 @@ module ActiveRecord class_name: name } - message_bus.instrument('instantiation.active_record', payload) do - result_set.map { |record| instantiate(record, column_types) } + message_bus.instrument("instantiation.active_record", payload) do + result_set.map { |record| instantiate(record, column_types, &block) } end end @@ -62,8 +62,7 @@ module ActiveRecord # # * +sql+ - An SQL statement which should return a count query from the database, see the example above. def count_by_sql(sql) - sql = sanitize_conditions(sql) - connection.select_value(sql, "#{name} Count").to_i + connection.select_value(sanitize_sql(sql), "#{name} Count").to_i end end end diff --git a/activerecord/lib/active_record/railtie.rb b/activerecord/lib/active_record/railtie.rb index da6b8447d3..2701c5bca9 100644 --- a/activerecord/lib/active_record/railtie.rb +++ b/activerecord/lib/active_record/railtie.rb @@ -3,7 +3,7 @@ require "rails" require "active_model/railtie" # For now, action_controller must always be present with -# rails, so let's make sure that it gets required before +# Rails, so let's make sure that it gets required before # here. This is needed for correctly setting up the middleware. # In the future, this might become an optional require. require "action_controller/railtie" @@ -13,23 +13,16 @@ module ActiveRecord class Railtie < Rails::Railtie # :nodoc: config.active_record = ActiveSupport::OrderedOptions.new - config.app_generators.orm :active_record, :migration => true, - :timestamps => true - - config.app_middleware.insert_after "::ActionDispatch::Callbacks", - "ActiveRecord::QueryCache" - - config.app_middleware.insert_after "::ActionDispatch::Callbacks", - "ActiveRecord::ConnectionAdapters::ConnectionManagement" + config.app_generators.orm :active_record, migration: true, + timestamps: true config.action_dispatch.rescue_responses.merge!( - 'ActiveRecord::RecordNotFound' => :not_found, - 'ActiveRecord::StaleObjectError' => :conflict, - 'ActiveRecord::RecordInvalid' => :unprocessable_entity, - 'ActiveRecord::RecordNotSaved' => :unprocessable_entity + "ActiveRecord::RecordNotFound" => :not_found, + "ActiveRecord::StaleObjectError" => :conflict, + "ActiveRecord::RecordInvalid" => :unprocessable_entity, + "ActiveRecord::RecordNotSaved" => :unprocessable_entity ) - config.active_record.use_schema_cache_dump = true config.active_record.maintain_test_schema = true @@ -40,9 +33,9 @@ module ActiveRecord task :load_config do ActiveRecord::Tasks::DatabaseTasks.database_configuration = Rails.application.config.database_configuration - if defined?(ENGINE_PATH) && engine = Rails::Engine.find(ENGINE_PATH) - if engine.paths['db/migrate'].existent - ActiveRecord::Tasks::DatabaseTasks.migrations_paths += engine.paths['db/migrate'].to_a + if defined?(ENGINE_ROOT) && engine = Rails::Engine.find(ENGINE_ROOT) + if engine.paths["db/migrate"].existent + ActiveRecord::Tasks::DatabaseTasks.migrations_paths += engine.paths["db/migrate"].to_a end end end @@ -57,8 +50,10 @@ module ActiveRecord console do |app| require "active_record/railties/console_sandbox" if app.sandbox? require "active_record/base" - console = ActiveSupport::Logger.new(STDERR) - Rails.logger.extend ActiveSupport::Logger.broadcast console + unless ActiveSupport::Logger.logger_outputs_to?(Rails.logger, STDERR, STDOUT) + console = ActiveSupport::Logger.new(STDERR) + Rails.logger.extend ActiveSupport::Logger.broadcast console + end end runner do @@ -78,8 +73,8 @@ module ActiveRecord initializer "active_record.migration_error" do if config.active_record.delete(:migration_error) == :page_load - config.app_middleware.insert_after "::ActionDispatch::Callbacks", - "ActiveRecord::Migration::CheckPending" + config.app_middleware.insert_after ::ActionDispatch::Callbacks, + ActiveRecord::Migration::CheckPending end end @@ -87,15 +82,15 @@ module ActiveRecord if config.active_record.delete(:use_schema_cache_dump) config.after_initialize do |app| ActiveSupport.on_load(:active_record) do - filename = File.join(app.config.paths["db"].first, "schema_cache.dump") + filename = File.join(app.config.paths["db"].first, "schema_cache.yml") if File.file?(filename) - cache = Marshal.load File.binread filename + cache = YAML.load(File.read(filename)) if cache.version == ActiveRecord::Migrator.current_version self.connection.schema_cache = cache self.connection_pool.schema_cache = cache.dup else - 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}." + warn "Ignoring db/schema_cache.yml 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 @@ -106,14 +101,14 @@ module ActiveRecord initializer "active_record.warn_on_records_fetched_greater_than" do if config.active_record.warn_on_records_fetched_greater_than ActiveSupport.on_load(:active_record) do - require 'active_record/relation/record_fetch_warning' + require "active_record/relation/record_fetch_warning" end end end initializer "active_record.set_configs" do |app| ActiveSupport.on_load(:active_record) do - app.config.active_record.each do |k,v| + app.config.active_record.each do |k, v| send "#{k}=", v end end @@ -121,7 +116,7 @@ module ActiveRecord # This sets the database configuration from Configuration#database_configuration # and then establishes the connection. - initializer "active_record.initialize_database" do |app| + initializer "active_record.initialize_database" do ActiveSupport.on_load(:active_record) do self.configurations = Rails.application.config.database_configuration @@ -134,8 +129,8 @@ Oops - You have a database configured, but it doesn't exist yet! Here's how to get started: 1. Configure your database in config/database.yml. - 2. Run `bin/rake db:create` to create the database. - 3. Run `bin/rake db:setup` to load your database schema. + 2. Run `bin/rails db:create` to create the database. + 3. Run `bin/rails db:setup` to load your database schema. end_warning raise end @@ -150,11 +145,9 @@ end_warning end end - initializer "active_record.set_reloader_hooks" do |app| - hook = app.config.reload_classes_only_on_change ? :to_prepare : :to_cleanup - + initializer "active_record.set_reloader_hooks" do ActiveSupport.on_load(:active_record) do - ActionDispatch::Reloader.send(hook) do + ActiveSupport::Reloader.before_class_unload do if ActiveRecord::Base.connected? ActiveRecord::Base.clear_cache! ActiveRecord::Base.clear_reloadable_connections! @@ -163,6 +156,12 @@ end_warning end end + initializer "active_record.set_executor_hooks" do + ActiveSupport.on_load(:active_record) do + ActiveRecord::QueryCache.install_executor_hooks + end + end + initializer "active_record.add_watchable_files" do |app| path = app.paths["db"].first config.watchable_files.concat ["#{path}/schema.rb", "#{path}/structure.sql"] diff --git a/activerecord/lib/active_record/railties/controller_runtime.rb b/activerecord/lib/active_record/railties/controller_runtime.rb index 8727e46cb3..8658188623 100644 --- a/activerecord/lib/active_record/railties/controller_runtime.rb +++ b/activerecord/lib/active_record/railties/controller_runtime.rb @@ -1,15 +1,19 @@ -require 'active_support/core_ext/module/attr_internal' -require 'active_record/log_subscriber' +require "active_support/core_ext/module/attr_internal" +require "active_record/log_subscriber" module ActiveRecord module Railties # :nodoc: module ControllerRuntime #:nodoc: extend ActiveSupport::Concern + # TODO Change this to private once we've dropped Ruby 2.2 support. + # Workaround for Ruby 2.2 "private attribute?" warning. protected attr_internal :db_runtime + private + def process_action(action, *args) # We also need to reset the runtime before each action # because of queries in middleware or in cases we are streaming @@ -19,7 +23,7 @@ module ActiveRecord end def cleanup_view_runtime - if logger.info? && ActiveRecord::Base.connected? + if logger && logger.info? && ActiveRecord::Base.connected? db_rt_before_render = ActiveRecord::LogSubscriber.reset_runtime self.db_runtime = (db_runtime || 0) + db_rt_before_render runtime = super diff --git a/activerecord/lib/active_record/railties/databases.rake b/activerecord/lib/active_record/railties/databases.rake index 6a72d528b4..25d79a6c7d 100644 --- a/activerecord/lib/active_record/railties/databases.rake +++ b/activerecord/lib/active_record/railties/databases.rake @@ -1,48 +1,62 @@ -require 'active_record' +require "active_record" db_namespace = namespace :db do + desc "Set the environment value for the database" + task "environment:set" => [:environment, :load_config] do + ActiveRecord::InternalMetadata.create_table + ActiveRecord::InternalMetadata[:environment] = ActiveRecord::Migrator.current_environment + end + + task check_protected_environments: [:environment, :load_config] do + ActiveRecord::Tasks::DatabaseTasks.check_protected_environments! + end + task :load_config do ActiveRecord::Base.configurations = ActiveRecord::Tasks::DatabaseTasks.database_configuration || {} ActiveRecord::Migrator.migrations_paths = ActiveRecord::Tasks::DatabaseTasks.migrations_paths end namespace :create do - task :all => :load_config do + task all: :load_config do ActiveRecord::Tasks::DatabaseTasks.create_all end end - desc 'Creates the database from DATABASE_URL or config/database.yml for the current RAILS_ENV (use db:create:all to create all databases in the config). Without RAILS_ENV, it defaults to creating the development and test databases.' - task :create => [:load_config] do + desc "Creates the database from DATABASE_URL or config/database.yml for the current RAILS_ENV (use db:create:all to create all databases in the config). Without RAILS_ENV or when RAILS_ENV is development, it defaults to creating the development and test databases." + task create: [:load_config] do ActiveRecord::Tasks::DatabaseTasks.create_current end namespace :drop do - task :all => :load_config do + task all: [:load_config, :check_protected_environments] do ActiveRecord::Tasks::DatabaseTasks.drop_all end end - desc 'Drops the database from DATABASE_URL or config/database.yml for the current RAILS_ENV (use db:drop:all to drop all databases in the config). Without RAILS_ENV, it defaults to dropping the development and test databases.' - task :drop => [:load_config] do + desc "Drops the database from DATABASE_URL or config/database.yml for the current RAILS_ENV (use db:drop:all to drop all databases in the config). Without RAILS_ENV or when RAILS_ENV is development, it defaults to dropping the development and test databases." + task drop: [:load_config, :check_protected_environments] do + db_namespace["drop:_unsafe"].invoke + end + + task "drop:_unsafe" => [:load_config] do ActiveRecord::Tasks::DatabaseTasks.drop_current end namespace :purge do - task :all => :load_config do + task all: [:load_config, :check_protected_environments] do ActiveRecord::Tasks::DatabaseTasks.purge_all end end - # desc "Empty the database from DATABASE_URL or config/database.yml for the current RAILS_ENV (use db:drop:all to drop all databases in the config). Without RAILS_ENV it defaults to purging the development and test databases." - task :purge => [:load_config] do + # desc "Empty the database from DATABASE_URL or config/database.yml for the current RAILS_ENV (use db:purge:all to purge all databases in the config). Without RAILS_ENV it defaults to purging the development and test databases." + task purge: [:load_config, :check_protected_environments] do ActiveRecord::Tasks::DatabaseTasks.purge_current end desc "Migrate the database (options: VERSION=x, VERBOSE=false, SCOPE=blog)." - task :migrate => [:environment, :load_config] do + task migrate: [:environment, :load_config] do ActiveRecord::Tasks::DatabaseTasks.migrate - db_namespace['_dump'].invoke + db_namespace["_dump"].invoke end # IMPORTANT: This task won't dump the schema if ActiveRecord::Base.dump_schema_after_migration is set to false @@ -57,59 +71,61 @@ db_namespace = namespace :db do end # Allow this task to be called as many times as required. An example is the # migrate:redo task, which calls other two internally that depend on this one. - db_namespace['_dump'].reenable + db_namespace["_dump"].reenable end namespace :migrate do # desc 'Rollbacks the database one migration and re migrate up (options: STEP=x, VERSION=x).' - task :redo => [:environment, :load_config] do - if ENV['VERSION'] - db_namespace['migrate:down'].invoke - db_namespace['migrate:up'].invoke + task redo: [:environment, :load_config] do + if ENV["VERSION"] + db_namespace["migrate:down"].invoke + db_namespace["migrate:up"].invoke else - db_namespace['rollback'].invoke - db_namespace['migrate'].invoke + db_namespace["rollback"].invoke + db_namespace["migrate"].invoke end end # desc 'Resets your database using your migrations for the current environment' - task :reset => ['db:drop', 'db:create', 'db:migrate'] + task reset: ["db:drop", "db:create", "db:migrate"] # desc 'Runs the "up" for a given migration VERSION.' - task :up => [:environment, :load_config] do - version = ENV['VERSION'] ? ENV['VERSION'].to_i : nil - raise 'VERSION is required' unless version - ActiveRecord::Migrator.run(:up, ActiveRecord::Migrator.migrations_paths, version) - db_namespace['_dump'].invoke + task up: [:environment, :load_config] do + version = ENV["VERSION"] ? ENV["VERSION"].to_i : nil + raise "VERSION is required" unless version + ActiveRecord::Migrator.run(:up, ActiveRecord::Tasks::DatabaseTasks.migrations_paths, version) + db_namespace["_dump"].invoke end # desc 'Runs the "down" for a given migration VERSION.' - task :down => [:environment, :load_config] do - version = ENV['VERSION'] ? ENV['VERSION'].to_i : nil - raise 'VERSION is required - To go down one migration, run db:rollback' unless version - ActiveRecord::Migrator.run(:down, ActiveRecord::Migrator.migrations_paths, version) - db_namespace['_dump'].invoke + task down: [:environment, :load_config] do + version = ENV["VERSION"] ? ENV["VERSION"].to_i : nil + raise "VERSION is required - To go down one migration, run db:rollback" unless version + ActiveRecord::Migrator.run(:down, ActiveRecord::Tasks::DatabaseTasks.migrations_paths, version) + db_namespace["_dump"].invoke end - desc 'Display status of migrations' - task :status => [:environment, :load_config] do + desc "Display status of migrations" + task status: [:environment, :load_config] do unless ActiveRecord::SchemaMigration.table_exists? - abort 'Schema migrations table does not exist yet.' + abort "Schema migrations table does not exist yet." end db_list = ActiveRecord::SchemaMigration.normalized_versions file_list = - ActiveRecord::Migrator.migrations_paths.flat_map do |path| - # match "20091231235959_some_name.rb" and "001_some_name.rb" pattern - Dir.foreach(path).grep(/^(\d{3,})_(.+)\.rb$/) do - version = ActiveRecord::SchemaMigration.normalize_migration_number($1) - status = db_list.delete(version) ? 'up' : 'down' - [status, version, $2.humanize] - end + ActiveRecord::Tasks::DatabaseTasks.migrations_paths.flat_map do |path| + Dir.foreach(path).map do |file| + next unless ActiveRecord::Migrator.match_to_migration_filename?(file) + + version, name, scope = ActiveRecord::Migrator.parse_migration_filename(file) + version = ActiveRecord::SchemaMigration.normalize_migration_number(version) + status = db_list.delete(version) ? "up" : "down" + [status, version, (name + scope).humanize] + end.compact end db_list.map! do |version| - ['up', version, '********** NO FILE **********'] + ["up", version, "********** NO FILE **********"] end # output puts "\ndatabase: #{ActiveRecord::Base.connection_config[:database]}\n\n" @@ -122,93 +138,93 @@ db_namespace = namespace :db do end end - desc 'Rolls the schema back to the previous version (specify steps w/ STEP=n).' - task :rollback => [:environment, :load_config] do - step = ENV['STEP'] ? ENV['STEP'].to_i : 1 - ActiveRecord::Migrator.rollback(ActiveRecord::Migrator.migrations_paths, step) - db_namespace['_dump'].invoke + desc "Rolls the schema back to the previous version (specify steps w/ STEP=n)." + task rollback: [:environment, :load_config] do + step = ENV["STEP"] ? ENV["STEP"].to_i : 1 + ActiveRecord::Migrator.rollback(ActiveRecord::Tasks::DatabaseTasks.migrations_paths, step) + db_namespace["_dump"].invoke end # desc 'Pushes the schema to the next version (specify steps w/ STEP=n).' - task :forward => [:environment, :load_config] do - step = ENV['STEP'] ? ENV['STEP'].to_i : 1 - ActiveRecord::Migrator.forward(ActiveRecord::Migrator.migrations_paths, step) - db_namespace['_dump'].invoke + task forward: [:environment, :load_config] do + step = ENV["STEP"] ? ENV["STEP"].to_i : 1 + ActiveRecord::Migrator.forward(ActiveRecord::Tasks::DatabaseTasks.migrations_paths, step) + db_namespace["_dump"].invoke end # desc 'Drops and recreates the database from db/schema.rb for the current environment and loads the seeds.' - task :reset => [ 'db:drop', 'db:setup' ] + task reset: [ "db:drop", "db:setup" ] # desc "Retrieves the charset for the current environment's database" - task :charset => [:environment, :load_config] do + task charset: [:environment, :load_config] do puts ActiveRecord::Tasks::DatabaseTasks.charset_current end # desc "Retrieves the collation for the current environment's database" - task :collation => [:environment, :load_config] do + task collation: [:environment, :load_config] do begin puts ActiveRecord::Tasks::DatabaseTasks.collation_current rescue NoMethodError - $stderr.puts 'Sorry, your database adapter is not supported yet. Feel free to submit a patch.' + $stderr.puts "Sorry, your database adapter is not supported yet. Feel free to submit a patch." end end - desc 'Retrieves the current schema version number' - task :version => [:environment, :load_config] do + desc "Retrieves the current schema version number" + task version: [:environment, :load_config] do puts "Current version: #{ActiveRecord::Migrator.current_version}" end # desc "Raises an error if there are pending migrations" - task :abort_if_pending_migrations => [:environment, :load_config] do - pending_migrations = ActiveRecord::Migrator.open(ActiveRecord::Migrator.migrations_paths).pending_migrations + task abort_if_pending_migrations: [:environment, :load_config] do + pending_migrations = ActiveRecord::Migrator.open(ActiveRecord::Tasks::DatabaseTasks.migrations_paths).pending_migrations if pending_migrations.any? puts "You have #{pending_migrations.size} pending #{pending_migrations.size > 1 ? 'migrations:' : 'migration:'}" pending_migrations.each do |pending_migration| - puts ' %4d %s' % [pending_migration.version, pending_migration.name] + puts " %4d %s" % [pending_migration.version, pending_migration.name] end - abort %{Run `rake db:migrate` to update your database then try again.} + abort %{Run `rails db:migrate` to update your database then try again.} end end - desc 'Creates the database, loads the schema, and initializes with the seed data (use db:reset to also drop the database first)' - task :setup => ['db:schema:load_if_ruby', 'db:structure:load_if_sql', :seed] + desc "Creates the database, loads the schema, and initializes with the seed data (use db:reset to also drop the database first)" + task setup: ["db:schema:load_if_ruby", "db:structure:load_if_sql", :seed] - desc 'Loads the seed data from db/seeds.rb' + desc "Loads the seed data from db/seeds.rb" task :seed do - db_namespace['abort_if_pending_migrations'].invoke + db_namespace["abort_if_pending_migrations"].invoke ActiveRecord::Tasks::DatabaseTasks.load_seed end namespace :fixtures do desc "Loads fixtures into the current environment's database. Load specific fixtures using FIXTURES=x,y. Load from subdirectory in test/fixtures using FIXTURES_DIR=z. Specify an alternative path (eg. spec/fixtures) using FIXTURES_PATH=spec/fixtures." - task :load => [:environment, :load_config] do - require 'active_record/fixtures' + task load: [:environment, :load_config] do + require "active_record/fixtures" base_dir = ActiveRecord::Tasks::DatabaseTasks.fixtures_path - fixtures_dir = if ENV['FIXTURES_DIR'] - File.join base_dir, ENV['FIXTURES_DIR'] - else - base_dir - end + fixtures_dir = if ENV["FIXTURES_DIR"] + File.join base_dir, ENV["FIXTURES_DIR"] + else + base_dir + end - fixture_files = if ENV['FIXTURES'] - ENV['FIXTURES'].split(',') - else - # The use of String#[] here is to support namespaced fixtures - Dir["#{fixtures_dir}/**/*.yml"].map {|f| f[(fixtures_dir.size + 1)..-5] } - end + fixture_files = if ENV["FIXTURES"] + ENV["FIXTURES"].split(",") + else + # The use of String#[] here is to support namespaced fixtures. + Dir["#{fixtures_dir}/**/*.yml"].map { |f| f[(fixtures_dir.size + 1)..-5] } + end ActiveRecord::FixtureSet.create_fixtures(fixtures_dir, fixture_files) end # desc "Search for a fixture given a LABEL or ID. Specify an alternative path (eg. spec/fixtures) using FIXTURES_PATH=spec/fixtures." - task :identify => [:environment, :load_config] do - require 'active_record/fixtures' + task identify: [:environment, :load_config] do + require "active_record/fixtures" - label, id = ENV['LABEL'], ENV['ID'] - raise 'LABEL or ID required' if label.blank? && id.blank? + 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::FixtureSet.identify(label)}.) if label @@ -229,49 +245,49 @@ db_namespace = namespace :db do end namespace :schema do - desc 'Creates a db/schema.rb file that is portable against any DB supported by AR' - task :dump => [:environment, :load_config] do - require 'active_record/schema_dumper' - filename = ENV['SCHEMA'] || File.join(ActiveRecord::Tasks::DatabaseTasks.db_dir, 'schema.rb') + desc "Creates a db/schema.rb file that is portable against any DB supported by Active Record" + task dump: [:environment, :load_config] do + require "active_record/schema_dumper" + filename = ENV["SCHEMA"] || File.join(ActiveRecord::Tasks::DatabaseTasks.db_dir, "schema.rb") File.open(filename, "w:utf-8") do |file| ActiveRecord::SchemaDumper.dump(ActiveRecord::Base.connection, file) end - db_namespace['schema:dump'].reenable + db_namespace["schema:dump"].reenable end - desc 'Loads a schema.rb file into the database' - task :load => [:environment, :load_config] do - ActiveRecord::Tasks::DatabaseTasks.load_schema_current(:ruby, ENV['SCHEMA']) + desc "Loads a schema.rb file into the database" + task load: [:environment, :load_config, :check_protected_environments] do + ActiveRecord::Tasks::DatabaseTasks.load_schema_current(:ruby, ENV["SCHEMA"]) end - task :load_if_ruby => ['db:create', :environment] do + task load_if_ruby: ["db:create", :environment] do db_namespace["schema:load"].invoke if ActiveRecord::Base.schema_format == :ruby end namespace :cache do - desc 'Creates a db/schema_cache.dump file.' - task :dump => [:environment, :load_config] do - con = ActiveRecord::Base.connection - filename = File.join(ActiveRecord::Tasks::DatabaseTasks.db_dir, "schema_cache.dump") - - con.schema_cache.clear! - con.tables.each { |table| con.schema_cache.add(table) } - open(filename, 'wb') { |f| f.write(Marshal.dump(con.schema_cache)) } + desc "Creates a db/schema_cache.yml file." + task dump: [:environment, :load_config] do + conn = ActiveRecord::Base.connection + filename = File.join(ActiveRecord::Tasks::DatabaseTasks.db_dir, "schema_cache.yml") + + conn.schema_cache.clear! + conn.data_sources.each { |table| conn.schema_cache.add(table) } + open(filename, "wb") { |f| f.write(YAML.dump(conn.schema_cache)) } end - desc 'Clears a db/schema_cache.dump file.' - task :clear => [:environment, :load_config] do - filename = File.join(ActiveRecord::Tasks::DatabaseTasks.db_dir, "schema_cache.dump") - FileUtils.rm(filename) if File.exist?(filename) + desc "Clears a db/schema_cache.yml file." + task clear: [:environment, :load_config] do + filename = File.join(ActiveRecord::Tasks::DatabaseTasks.db_dir, "schema_cache.yml") + rm_f filename, verbose: false end end end namespace :structure do - desc 'Dumps the database structure to db/structure.sql. Specify another file with SCHEMA=db/my_structure.sql' - task :dump => [:environment, :load_config] do - filename = ENV['SCHEMA'] || File.join(ActiveRecord::Tasks::DatabaseTasks.db_dir, "structure.sql") + desc "Dumps the database structure to db/structure.sql. Specify another file with SCHEMA=db/my_structure.sql" + task dump: [:environment, :load_config] do + filename = ENV["SCHEMA"] || File.join(ActiveRecord::Tasks::DatabaseTasks.db_dir, "structure.sql") current_config = ActiveRecord::Tasks::DatabaseTasks.current_config ActiveRecord::Tasks::DatabaseTasks.structure_dump(current_config, filename) @@ -282,15 +298,15 @@ db_namespace = namespace :db do f.print "\n" end end - db_namespace['structure:dump'].reenable + db_namespace["structure:dump"].reenable end desc "Recreates the databases from the structure.sql file" - task :load => [:load_config] do - ActiveRecord::Tasks::DatabaseTasks.load_schema_current(:sql, ENV['SCHEMA']) + task load: [:environment, :load_config, :check_protected_environments] do + ActiveRecord::Tasks::DatabaseTasks.load_schema_current(:sql, ENV["SCHEMA"]) end - task :load_if_sql => ['db:create', :environment] do + task load_if_sql: ["db:create", :environment] do db_namespace["structure:load"].invoke if ActiveRecord::Base.schema_format == :sql end end @@ -305,21 +321,21 @@ db_namespace = namespace :db do end # desc "Recreate the test database from the current schema" - task :load => %w(db:test:purge) do + task load: %w(db:test:purge) do case ActiveRecord::Base.schema_format - when :ruby - db_namespace["test:load_schema"].invoke - when :sql - db_namespace["test:load_structure"].invoke + when :ruby + db_namespace["test:load_schema"].invoke + when :sql + db_namespace["test:load_structure"].invoke end end # desc "Recreate the test database from an existent schema.rb file" - task :load_schema => %w(db:test:purge) do + task load_schema: %w(db:test:purge) do begin should_reconnect = ActiveRecord::Base.connection_pool.active_connection? ActiveRecord::Schema.verbose = false - ActiveRecord::Tasks::DatabaseTasks.load_schema ActiveRecord::Base.configurations['test'], :ruby, ENV['SCHEMA'] + ActiveRecord::Tasks::DatabaseTasks.load_schema ActiveRecord::Base.configurations["test"], :ruby, ENV["SCHEMA"] ensure if should_reconnect ActiveRecord::Base.establish_connection(ActiveRecord::Base.configurations[ActiveRecord::Tasks::DatabaseTasks.env]) @@ -328,35 +344,35 @@ db_namespace = namespace :db do end # desc "Recreate the test database from an existent structure.sql file" - task :load_structure => %w(db:test:purge) do - ActiveRecord::Tasks::DatabaseTasks.load_schema ActiveRecord::Base.configurations['test'], :sql, ENV['SCHEMA'] + task load_structure: %w(db:test:purge) do + ActiveRecord::Tasks::DatabaseTasks.load_schema ActiveRecord::Base.configurations["test"], :sql, ENV["SCHEMA"] end # desc "Recreate the test database from a fresh schema" - task :clone => %w(db:test:deprecated environment) do + task clone: %w(db:test:deprecated environment) do case ActiveRecord::Base.schema_format - when :ruby - db_namespace["test:clone_schema"].invoke - when :sql - db_namespace["test:clone_structure"].invoke + when :ruby + db_namespace["test:clone_schema"].invoke + when :sql + db_namespace["test:clone_structure"].invoke end end # desc "Recreate the test database from a fresh schema.rb file" - task :clone_schema => %w(db:test:deprecated db:schema:dump db:test:load_schema) + task clone_schema: %w(db:test:deprecated db:schema:dump db:test:load_schema) # desc "Recreate the test database from a fresh structure.sql file" - task :clone_structure => %w(db:test:deprecated db:structure:dump db:test:load_structure) + task clone_structure: %w(db:test:deprecated db:structure:dump db:test:load_structure) # desc "Empty the test database" - task :purge => %w(environment load_config) do - ActiveRecord::Tasks::DatabaseTasks.purge ActiveRecord::Base.configurations['test'] + task purge: %w(environment load_config check_protected_environments) do + ActiveRecord::Tasks::DatabaseTasks.purge ActiveRecord::Base.configurations["test"] end - # desc 'Check for pending migrations and load the test schema' - task :prepare => %w(environment load_config) do + # desc 'Load the test schema' + task prepare: %w(environment load_config) do unless ActiveRecord::Base.configurations.blank? - db_namespace['test:load'].invoke + db_namespace["test:load"].invoke end end end @@ -365,13 +381,13 @@ end namespace :railties do namespace :install do # desc "Copies missing migrations from Railties (e.g. engines). You can specify Railties to use with FROM=railtie1,railtie2" - task :migrations => :'db:load_config' do - to_load = ENV['FROM'].blank? ? :all : ENV['FROM'].split(",").map(&:strip) + task migrations: :'db:load_config' do + to_load = ENV["FROM"].blank? ? :all : ENV["FROM"].split(",").map(&:strip) railties = {} Rails.application.migration_railties.each do |railtie| next unless to_load == :all || to_load.include?(railtie.railtie_name) - if railtie.respond_to?(:paths) && (path = railtie.paths['db/migrate'].first) + if railtie.respond_to?(:paths) && (path = railtie.paths["db/migrate"].first) railties[railtie.railtie_name] = path end end @@ -384,8 +400,8 @@ namespace :railties do puts "Copied migration #{migration.basename} from #{name}" end - ActiveRecord::Migration.copy(ActiveRecord::Migrator.migrations_paths.first, railties, - :on_skip => on_skip, :on_copy => on_copy) + ActiveRecord::Migration.copy(ActiveRecord::Tasks::DatabaseTasks.migrations_paths.first, railties, + on_skip: on_skip, on_copy: on_copy) end end end diff --git a/activerecord/lib/active_record/railties/jdbcmysql_error.rb b/activerecord/lib/active_record/railties/jdbcmysql_error.rb index 6a38211bff..d7cf4df339 100644 --- a/activerecord/lib/active_record/railties/jdbcmysql_error.rb +++ b/activerecord/lib/active_record/railties/jdbcmysql_error.rb @@ -3,7 +3,7 @@ module ArJdbcMySQL #:nodoc: class Error < StandardError #:nodoc: attr_accessor :error_number, :sql_state - def initialize msg + def initialize(msg) super @error_number = nil @sql_state = nil diff --git a/activerecord/lib/active_record/readonly_attributes.rb b/activerecord/lib/active_record/readonly_attributes.rb index ce78f1756d..8ff265bdfa 100644 --- a/activerecord/lib/active_record/readonly_attributes.rb +++ b/activerecord/lib/active_record/readonly_attributes.rb @@ -16,7 +16,7 @@ module ActiveRecord # Returns an array of all the attributes that have been specified as readonly. def readonly_attributes - self._attr_readonly + _attr_readonly end end end diff --git a/activerecord/lib/active_record/reflection.rb b/activerecord/lib/active_record/reflection.rb index 5360db6a19..f3e81ee1e2 100644 --- a/activerecord/lib/active_record/reflection.rb +++ b/activerecord/lib/active_record/reflection.rb @@ -1,5 +1,5 @@ -require 'thread' -require 'active_support/core_ext/string/filters' +require "thread" +require "active_support/core_ext/string/filters" module ActiveRecord # = Active Record Reflection @@ -7,31 +7,33 @@ module ActiveRecord extend ActiveSupport::Concern included do - class_attribute :_reflections - class_attribute :aggregate_reflections + class_attribute :_reflections, instance_writer: false + class_attribute :aggregate_reflections, instance_writer: false self._reflections = {} self.aggregate_reflections = {} end def self.create(macro, name, scope, options, ar) - klass = case macro - when :composed_of - AggregateReflection - when :has_many - HasManyReflection - when :has_one - HasOneReflection - when :belongs_to - BelongsToReflection - else - raise "Unsupported Macro: #{macro}" - end + klass = \ + case macro + when :composed_of + AggregateReflection + when :has_many + HasManyReflection + when :has_one + HasOneReflection + when :belongs_to + BelongsToReflection + else + raise "Unsupported Macro: #{macro}" + end reflection = klass.new(name, scope, options, ar) options[:through] ? ThroughReflection.new(reflection) : reflection end def self.add_reflection(ar, name, reflection) + ar.clear_reflections_cache ar._reflections = ar._reflections.merge(name.to_s => reflection) end @@ -61,24 +63,27 @@ module ActiveRecord aggregate_reflections[aggregation.to_s] end - # Returns a Hash of name of the reflection as the key and a AssociationReflection as the value. + # Returns a Hash of name of the reflection as the key and an AssociationReflection as the value. # # Account.reflections # => {"balance" => AggregateReflection} # - # @api public def reflections - ref = {} - _reflections.each do |name, reflection| - parent_reflection = reflection.parent_reflection + @__reflections ||= begin + ref = {} - if parent_reflection - parent_name = parent_reflection.name - ref[parent_name.to_s] = parent_reflection - else - ref[name] = reflection + _reflections.each do |name, reflection| + parent_reflection = reflection.parent_reflection + + if parent_reflection + parent_name = parent_reflection.name + ref[parent_name.to_s] = parent_reflection + else + ref[name] = reflection + end end + + ref end - ref end # Returns an array of AssociationReflection objects for all the @@ -91,10 +96,10 @@ module ActiveRecord # Account.reflect_on_all_associations # returns an array of all associations # Account.reflect_on_all_associations(:has_many) # returns an array of all has_many associations # - # @api public def reflect_on_all_associations(macro = nil) association_reflections = reflections.values - macro ? association_reflections.select { |reflection| reflection.macro == macro } : association_reflections + association_reflections.select! { |reflection| reflection.macro == macro } if macro + association_reflections end # Returns the AssociationReflection object for the +association+ (use the symbol). @@ -102,27 +107,42 @@ module ActiveRecord # Account.reflect_on_association(:owner) # returns the owner AssociationReflection # Invoice.reflect_on_association(:line_items).macro # returns :has_many # - # @api public def reflect_on_association(association) reflections[association.to_s] end - # @api private def _reflect_on_association(association) #:nodoc: _reflections[association.to_s] end # Returns an array of AssociationReflection objects for all associations which have <tt>:autosave</tt> enabled. - # - # @api public def reflect_on_all_autosave_associations reflections.values.select { |reflection| reflection.options[:autosave] } end + + def clear_reflections_cache # :nodoc: + @__reflections = nil + end end - # Holds all the methods that are shared between MacroReflection, AssociationReflection - # and ThroughReflection + # Holds all the methods that are shared between MacroReflection and ThroughReflection. + # + # AbstractReflection + # MacroReflection + # AggregateReflection + # AssociationReflection + # HasManyReflection + # HasOneReflection + # BelongsToReflection + # HasAndBelongsToManyReflection + # ThroughReflection + # PolymorphicReflection + # RuntimeReflection class AbstractReflection # :nodoc: + def through_reflection? + false + end + def table_name klass.table_name end @@ -159,21 +179,79 @@ module ActiveRecord scope_chain.flatten end + def counter_cache_column + if belongs_to? + if options[:counter_cache] == true + "#{active_record.name.demodulize.underscore.pluralize}_count" + elsif options[:counter_cache] + options[:counter_cache].to_s + end + else + options[:counter_cache] ? options[:counter_cache].to_s : "#{name}_count" + end + end + + def inverse_of + return unless inverse_name + + @inverse_of ||= klass._reflect_on_association inverse_name + end + + def check_validity_of_inverse! + unless polymorphic? + if has_inverse? && inverse_of.nil? + raise InverseOfAssociationNotFoundError.new(self) + end + end + end + + # This shit is nasty. We need to avoid the following situation: + # + # * An associated record is deleted via record.destroy + # * Hence the callbacks run, and they find a belongs_to on the record with a + # :counter_cache options which points back at our owner. So they update the + # counter cache. + # * In which case, we must make sure to *not* update the counter cache, or else + # it will be decremented twice. + # + # Hence this method. + def inverse_which_updates_counter_cache + return @inverse_which_updates_counter_cache if defined?(@inverse_which_updates_counter_cache) + @inverse_which_updates_counter_cache = klass.reflect_on_all_associations(:belongs_to).find do |inverse| + inverse.counter_cache_column == counter_cache_column + end + end + alias inverse_updates_counter_cache? inverse_which_updates_counter_cache + + def inverse_updates_counter_in_memory? + inverse_of && inverse_which_updates_counter_cache == inverse_of + end + + # Returns whether a counter cache should be used for this association. + # + # The counter_cache option must be given on either the owner or inverse + # association, and the column must be present on the owner. + def has_cached_counter? + options[:counter_cache] || + inverse_which_updates_counter_cache && inverse_which_updates_counter_cache.options[:counter_cache] && + !!active_record.columns_hash[counter_cache_column] + end + + def counter_must_be_updated_by_has_many? + !inverse_updates_counter_in_memory? && has_cached_counter? + end + def alias_candidate(name) "#{plural_name}_#{name}" end + + def chain + collect_join_chain + end end # Base class for AggregateReflection and AssociationReflection. Objects of # AggregateReflection and AssociationReflection are returned by the Reflection::ClassMethods. - # - # MacroReflection - # AggregateReflection - # AssociationReflection - # HasManyReflection - # HasOneReflection - # BelongsToReflection - # ThroughReflection class MacroReflection < AbstractReflection # Returns the name of the macro. # @@ -204,7 +282,6 @@ module ActiveRecord end def autosave=(autosave) - @automatic_inverse_of = false @options[:autosave] = autosave parent_reflection = self.parent_reflection if parent_reflection @@ -234,13 +311,16 @@ module ActiveRecord active_record == other_aggregation.active_record end + def scope_for(klass) + scope ? klass.unscoped.instance_exec(nil, &scope) : klass.unscoped + end + private def derive_class_name name.to_s.camelize end end - # Holds all the meta-data about an aggregation as it was specified in the # Active Record class. class AggregateReflection < MacroReflection #:nodoc: @@ -305,7 +385,7 @@ module ActiveRecord end def foreign_key - @foreign_key ||= options[:foreign_key] || derive_foreign_key + @foreign_key ||= options[:foreign_key] || derive_foreign_key.freeze end def association_foreign_key @@ -317,30 +397,18 @@ module ActiveRecord options[:primary_key] || primary_key(klass || self.klass) end - def active_record_primary_key - @active_record_primary_key ||= options[:primary_key] || primary_key(active_record) + def association_primary_key_type + klass.type_for_attribute(association_primary_key) end - def counter_cache_column - if options[:counter_cache] == true - "#{active_record.name.demodulize.underscore.pluralize}_count" - elsif options[:counter_cache] - options[:counter_cache].to_s - end + def active_record_primary_key + @active_record_primary_key ||= options[:primary_key] || primary_key(active_record) end def check_validity! check_validity_of_inverse! end - def check_validity_of_inverse! - unless polymorphic? - if has_inverse? && inverse_of.nil? - raise InverseOfAssociationNotFoundError.new(self) - end - end - end - def check_preloadable! return unless scope @@ -368,7 +436,7 @@ module ActiveRecord # A chain of reflections from this one back to the owner. For more see the explanation in # ThroughReflection. - def chain + def collect_join_chain [self] end @@ -388,14 +456,12 @@ module ActiveRecord scope ? [[scope]] : [[]] end - def has_inverse? - inverse_name + def has_scope? + scope end - def inverse_of - return unless inverse_name - - @inverse_of ||= klass._reflect_on_association inverse_name + def has_inverse? + inverse_name end def polymorphic_inverse_of(associated_class) @@ -439,28 +505,7 @@ module ActiveRecord # Returns +true+ if +self+ is a +has_one+ reflection. def has_one?; false; end - def association_class - case macro - when :belongs_to - if polymorphic? - Associations::BelongsToPolymorphicAssociation - else - Associations::BelongsToAssociation - end - when :has_many - if options[:through] - Associations::HasManyThroughAssociation - else - Associations::HasManyAssociation - end - when :has_one - if options[:through] - Associations::HasOneThroughAssociation - else - Associations::HasOneAssociation - end - end - end + def association_class; raise NotImplementedError; end def polymorphic? options[:polymorphic] @@ -469,6 +514,18 @@ module ActiveRecord VALID_AUTOMATIC_INVERSE_MACROS = [:has_many, :has_one, :belongs_to] INVALID_AUTOMATIC_INVERSE_OPTIONS = [:conditions, :through, :polymorphic, :foreign_key] + def add_as_source(seed) + seed + end + + def add_as_polymorphic_through(reflection, seed) + seed + [PolymorphicReflection.new(self, reflection)] + end + + def add_as_through(seed) + seed + [self] + end + protected def actual_source_reflection # FIXME: this is a horrible name @@ -478,30 +535,19 @@ module ActiveRecord private def calculate_constructable(macro, options) - case macro - when :belongs_to - !polymorphic? - when :has_one - !options[:through] - else - true - end + true end # Attempts to find the inverse association name automatically. # If it cannot find a suitable inverse association name, it returns - # nil. + # +nil+. def inverse_name options.fetch(:inverse_of) do - if @automatic_inverse_of == false - nil - else - @automatic_inverse_of ||= automatic_inverse_of - end + @automatic_inverse_of ||= automatic_inverse_of end end - # returns either nil or the inverse association name that it finds. + # returns either false or the inverse association name that it finds. def automatic_inverse_of if can_find_inverse_of_automatically?(self) inverse_name = ActiveSupport::Inflector.underscore(options[:as] || active_record.name.demodulize).to_sym @@ -578,34 +624,52 @@ module ActiveRecord end class HasManyReflection < AssociationReflection # :nodoc: - def initialize(name, scope, options, active_record) - super(name, scope, options, active_record) - end - def macro; :has_many; end def collection?; true; end - end - class HasOneReflection < AssociationReflection # :nodoc: - def initialize(name, scope, options, active_record) - super(name, scope, options, active_record) + def association_class + if options[:through] + Associations::HasManyThroughAssociation + else + Associations::HasManyAssociation + end end + end + class HasOneReflection < AssociationReflection # :nodoc: def macro; :has_one; end def has_one?; true; end - end - class BelongsToReflection < AssociationReflection # :nodoc: - def initialize(name, scope, options, active_record) - super(name, scope, options, active_record) + def association_class + if options[:through] + Associations::HasOneThroughAssociation + else + Associations::HasOneAssociation + end end + private + + def calculate_constructable(macro, options) + !options[:through] + end + end + + class BelongsToReflection < AssociationReflection # :nodoc: def macro; :belongs_to; end def belongs_to?; true; end + def association_class + if polymorphic? + Associations::BelongsToPolymorphicAssociation + else + Associations::BelongsToAssociation + end + end + def join_keys(association_klass) key = polymorphic? ? association_primary_key(association_klass) : association_primary_key JoinKeys.new(key, foreign_key) @@ -614,6 +678,12 @@ module ActiveRecord def join_id_for(owner) # :nodoc: owner[foreign_key] end + + private + + def calculate_constructable(macro, options) + !polymorphic? + end end class HasAndBelongsToManyReflection < AssociationReflection # :nodoc: @@ -633,14 +703,18 @@ module ActiveRecord class ThroughReflection < AbstractReflection #:nodoc: attr_reader :delegate_reflection delegate :foreign_key, :foreign_type, :association_foreign_key, - :active_record_primary_key, :type, :to => :source_reflection + :active_record_primary_key, :type, to: :source_reflection def initialize(delegate_reflection) @delegate_reflection = delegate_reflection - @klass = delegate_reflection.options[:anonymous_class] + @klass = delegate_reflection.options[:anonymous_class] @source_reflection_name = delegate_reflection.options[:source] end + def through_reflection? + true + end + def klass @klass ||= delegate_reflection.compute_class(class_name) end @@ -699,25 +773,13 @@ module ActiveRecord # # => [<ActiveRecord::Reflection::ThroughReflection: @delegate_reflection=#<ActiveRecord::Reflection::HasManyReflection: @name=:tags...>, # <ActiveRecord::Reflection::HasManyReflection: @name=:taggings, @options={}, @active_record=Post>] # - def chain - @chain ||= begin - a = source_reflection.chain - b = through_reflection.chain.map(&:dup) - - if options[:source_type] - b[0] = PolymorphicReflection.new(b[0], self) - end - - chain = a + b - chain[0] = self # Use self so we don't lose the information from :source_type - chain - end + def collect_join_chain + collect_join_reflections [self] end # This is for clearing cache on the reflection. Useful for tests that need to compare # SQL queries on associations. def clear_association_scope_cache # :nodoc: - @chain = nil delegate_reflection.clear_association_scope_cache source_reflection.clear_association_scope_cache through_reflection.clear_association_scope_cache @@ -764,13 +826,19 @@ module ActiveRecord end end + def has_scope? + scope || options[:source_type] || + source_reflection.has_scope? || + through_reflection.has_scope? + end + def join_keys(association_klass) source_reflection.join_keys(association_klass) end # A through association is nested if there would be more than one join table def nested? - chain.length > 2 + source_reflection.through_reflection? || through_reflection.through_reflection? end # We want to use the klass from this reflection, rather than just delegate straight to @@ -782,6 +850,10 @@ module ActiveRecord actual_source_reflection.options[:primary_key] || primary_key(klass || self.klass) end + def association_primary_key_type + klass.type_for_attribute(association_primary_key) + end + # Gets an array of possible <tt>:through</tt> source reflection names in both singular and plural form. # # class Post < ActiveRecord::Base @@ -806,15 +878,13 @@ module ActiveRecord } if names.length > 1 - example_options = options.dup - example_options[:source] = source_reflection_names.first - ActiveSupport::Deprecation.warn \ - "Ambiguous source reflection for through association. Please " \ - "specify a :source directive on your declaration like:\n" \ - "\n" \ - " class #{active_record.name} < ActiveRecord::Base\n" \ - " #{macro} :#{name}, #{example_options}\n" \ - " end" + raise AmbiguousSourceReflectionForThroughAssociation.new( + active_record.name, + macro, + name, + options, + source_reflection_names + ) end @source_reflection_name = names.first @@ -870,6 +940,27 @@ module ActiveRecord scope_chain end + def add_as_source(seed) + collect_join_reflections seed + end + + def add_as_polymorphic_through(reflection, seed) + collect_join_reflections(seed + [PolymorphicReflection.new(self, reflection)]) + end + + def add_as_through(seed) + collect_join_reflections(seed + [self]) + end + + def collect_join_reflections(seed) + a = source_reflection.add_as_source seed + if options[:source_type] + through_reflection.add_as_polymorphic_through self, a + else + through_reflection.add_as_through a + end + end + protected def actual_source_reflection # FIXME: this is a horrible name @@ -880,6 +971,8 @@ module ActiveRecord klass.primary_key || raise(UnknownPrimaryKey.new(klass)) end + def inverse_name; delegate_reflection.send(:inverse_name); end + private def derive_class_name # get the class_name of the belongs_to association of the through reflection @@ -890,10 +983,9 @@ module ActiveRecord public_instance_methods delegate(*delegate_methods, to: :delegate_reflection) - end - class PolymorphicReflection < ThroughReflection # :nodoc: + class PolymorphicReflection < AbstractReflection # :nodoc: def initialize(reflection, previous_reflection) @reflection = reflection @previous_reflection = previous_reflection @@ -924,7 +1016,7 @@ module ActiveRecord end def constraints - [source_type_info] + @reflection.constraints + [source_type_info] end def source_type_info diff --git a/activerecord/lib/active_record/relation.rb b/activerecord/lib/active_record/relation.rb index e4df122af6..ccd75ec5d2 100644 --- a/activerecord/lib/active_record/relation.rb +++ b/activerecord/lib/active_record/relation.rb @@ -1,11 +1,8 @@ -# -*- coding: utf-8 -*- -require "arel/collectors/bind" - module ActiveRecord - # = Active Record Relation + # = Active Record \Relation class Relation MULTI_VALUE_METHODS = [:includes, :eager_load, :preload, :select, :group, - :order, :joins, :references, + :order, :joins, :left_joins, :left_outer_joins, :references, :extending, :unscope] SINGLE_VALUE_METHODS = [:limit, :offset, :lock, :readonly, :reordering, @@ -32,9 +29,7 @@ module ActiveRecord end def initialize_copy(other) - # 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 = @values.dup reset end @@ -46,9 +41,9 @@ module ActiveRecord k.name == primary_key }] - if !primary_key_value && connection.prefetch_primary_key?(klass.table_name) - primary_key_value = connection.next_sequence_value(klass.sequence_name) - values[klass.arel_table[klass.primary_key]] = primary_key_value + if !primary_key_value && klass.prefetch_primary_key? + primary_key_value = klass.next_sequence_value + values[arel_attribute(klass.primary_key)] = primary_key_value end end @@ -65,8 +60,8 @@ module ActiveRecord @klass.connection.insert( im, - 'SQL', - primary_key, + "SQL", + primary_key || false, primary_key_value, nil, binds) @@ -89,27 +84,31 @@ module ActiveRecord @klass.connection.update( um, - 'SQL', + "SQL", bvs, ) end def substitute_values(values) # :nodoc: - binds = values.map do |arel_attr, value| - QueryAttribute.new(arel_attr.name, value, klass.type_for_attribute(arel_attr.name)) - end + binds = [] + substitutes = [] - substitutes = values.map do |(arel_attr, _)| - [arel_attr, connection.substitute_at(klass.columns_hash[arel_attr.name])] + values.each do |arel_attr, value| + binds.push QueryAttribute.new(arel_attr.name, value, klass.type_for_attribute(arel_attr.name)) + substitutes.push [arel_attr, Arel::Nodes::BindParam.new] end [substitutes, binds] end + def arel_attribute(name) # :nodoc: + klass.arel_attribute(name, table) + end + # Initializes new record from relation while maintaining the current # scope. # - # Expects arguments in the same format as +Base.new+. + # Expects arguments in the same format as {ActiveRecord::Base.new}[rdoc-ref:Core.new]. # # users = User.where(name: 'DHH') # user = users.new # => #<User id: nil, name: "DHH", created_at: nil, updated_at: nil> @@ -127,28 +126,32 @@ module ActiveRecord # Tries to create a new record with the same scoped attributes # defined in the relation. Returns the initialized object if validation fails. # - # Expects arguments in the same format as +Base.create+. + # Expects arguments in the same format as + # {ActiveRecord::Base.create}[rdoc-ref:Persistence::ClassMethods#create]. # # ==== Examples + # # users = User.where(name: 'Oscar') - # users.create # #<User id: 3, name: "oscar", ...> + # users.create # => #<User id: 3, name: "Oscar", ...> # # users.create(name: 'fxn') - # users.create # #<User id: 4, name: "fxn", ...> + # users.create # => #<User id: 4, name: "fxn", ...> # # users.create { |user| user.name = 'tenderlove' } - # # #<User id: 5, name: "tenderlove", ...> + # # => #<User id: 5, name: "tenderlove", ...> # # users.create(name: nil) # validation on name - # # #<User id: nil, name: nil, ...> + # # => #<User id: nil, name: nil, ...> def create(*args, &block) scoping { @klass.create(*args, &block) } end - # Similar to #create, but calls +create!+ on the base class. Raises - # an exception if a validation error occurs. + # Similar to #create, but calls + # {create!}[rdoc-ref:Persistence::ClassMethods#create!] + # on the base class. Raises an exception if a validation error occurs. # - # Expects arguments in the same format as <tt>Base.create!</tt>. + # Expects arguments in the same format as + # {ActiveRecord::Base.create!}[rdoc-ref:Persistence::ClassMethods#create!]. def create!(*args, &block) scoping { @klass.create!(*args, &block) } end @@ -182,7 +185,7 @@ module ActiveRecord # User.create_with(last_name: 'Johansson').find_or_create_by(first_name: 'Scarlett') # # => #<User id: 2, first_name: "Scarlett", last_name: "Johansson"> # - # This method accepts a block, which is passed down to +create+. The last example + # This method accepts a block, which is passed down to #create. The last example # above can be alternatively written this way: # # # Find the first user named "Scarlett" or create a new one with a @@ -194,7 +197,7 @@ module ActiveRecord # # This method always returns a record, but if creation was attempted and # failed due to validation errors it won't be persisted, you get what - # +create+ returns in such situation. + # #create returns in such situation. # # Please note *this method is not atomic*, it runs first a SELECT, and if # there are no results an INSERT is attempted. If there are other threads @@ -217,13 +220,15 @@ module ActiveRecord find_by(attributes) || create(attributes, &block) end - # Like <tt>find_or_create_by</tt>, but calls <tt>create!</tt> so an exception + # Like #find_or_create_by, but calls + # {create!}[rdoc-ref:Persistence::ClassMethods#create!] so an exception # is raised if the created record is invalid. def find_or_create_by!(attributes, &block) find_by(attributes) || create!(attributes, &block) end - # Like <tt>find_or_create_by</tt>, but calls <tt>new</tt> instead of <tt>create</tt>. + # Like #find_or_create_by, but calls {new}[rdoc-ref:Core#new] + # instead of {create}[rdoc-ref:Persistence::ClassMethods#create]. def find_or_initialize_by(attributes, &block) find_by(attributes) || new(attributes, &block) end @@ -238,23 +243,26 @@ module ActiveRecord # Please see further details in the # {Active Record Query Interface guide}[http://guides.rubyonrails.org/active_record_querying.html#running-explain]. def explain - #TODO: Fix for binds. exec_explain(collecting_queries_for_explain { exec_queries }) end # Converts relation objects to Array. def to_a + records.dup + end + + def records # :nodoc: load @records end # Serializes the relation objects Array. def encode_with(coder) - coder.represent_seq(nil, to_a) + coder.represent_seq(nil, records) end def as_json(options = nil) #:nodoc: - to_a.as_json(options) + records.as_json(options) end # Returns size of the records. @@ -266,12 +274,7 @@ module ActiveRecord def empty? return @records.empty? if loaded? - if limit_value == 0 - true - else - c = count(:all) - c.respond_to?(:zero?) ? c.zero? : c.empty? - end + limit_value == 0 || !exists? end # Returns true if there are no records. @@ -289,13 +292,39 @@ module ActiveRecord # Returns true if there is exactly one record. def one? return super if block_given? - limit_value ? to_a.one? : size == 1 + limit_value ? records.one? : size == 1 end # Returns true if there is more than one record. def many? return super if block_given? - limit_value ? to_a.many? : size > 1 + limit_value ? records.many? : size > 1 + end + + # Returns a cache key that can be used to identify the records fetched by + # this query. The cache key is built with a fingerprint of the sql query, + # the number of records matched by the query and a timestamp of the last + # updated record. When a new record comes to match the query, or any of + # the existing records is updated or deleted, the cache key changes. + # + # Product.where("name like ?", "%Cosmic Encounter%").cache_key + # # => "products/query-1850ab3d302391b85b8693e941286659-1-20150714212553907087000" + # + # If the collection is loaded, the method will iterate through the records + # to generate the timestamp, otherwise it will trigger one SQL query like: + # + # SELECT COUNT(*), MAX("products"."updated_at") FROM "products" WHERE (name like '%Cosmic Encounter%') + # + # You can also pass a custom timestamp column to fetch the timestamp of the + # last updated record. + # + # Product.where("name like ?", "%Game%").cache_key(:last_reviewed_at) + # + # You can customize the strategy to generate the key on a per model basis + # overriding ActiveRecord::Base#collection_cache_key. + def cache_key(timestamp_column = :updated_at) + @cache_keys ||= {} + @cache_keys[timestamp_column] ||= @klass.collection_cache_key(self, timestamp_column) end # Scope all queries to the current scope. @@ -316,9 +345,8 @@ module ActiveRecord # Updates all records in the current relation with details given. This method constructs a single SQL UPDATE # statement and sends it straight to the database. It does not instantiate the involved models and it does not - # trigger Active Record callbacks or validations. Values passed to `update_all` will not go through - # ActiveRecord's type-casting behavior. It should receive only values that can be passed as-is to the SQL - # database. + # trigger Active Record callbacks or validations. However, values passed to #update_all will still go through + # Active Record's normal type casting and serialization. # # ==== Parameters # @@ -334,6 +362,9 @@ module ActiveRecord # # # 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') + # + # # Update all invoices and set the number column to its id value. + # Invoice.update_all('number = id') def update_all(updates) raise ArgumentError, "Empty list of attributes to change" if updates.blank? @@ -341,17 +372,17 @@ module ActiveRecord stmt.set Arel.sql(@klass.send(:sanitize_sql_for_assignment, updates)) stmt.table(table) - stmt.key = table[primary_key] - if joins_values.any? - @klass.connection.join_to_update(stmt, arel) + if has_join_values? + @klass.connection.join_to_update(stmt, arel, arel_attribute(primary_key)) else + stmt.key = arel_attribute(primary_key) stmt.take(arel.limit) stmt.order(*arel.orders) stmt.wheres = arel.constraints end - @klass.connection.update stmt, 'SQL', bound_attributes + @klass.connection.update stmt, "SQL", bound_attributes end # Updates an object (or multiple objects) and saves it to the database, if validations pass. @@ -375,27 +406,34 @@ module ActiveRecord # people = Person.where(group: 'expert') # people.update(group: 'masters') # - # Note: Updating a large number of records will run a - # UPDATE query for each record, which may cause a performance - # issue. So if it is not needed to run callbacks for each update, it is - # preferred to use <tt>update_all</tt> for updating all records using - # a single query. + # Note: Updating a large number of records will run an + # UPDATE query for each record, which may cause a performance + # issue. So if it is not needed to run callbacks for each update, it is + # preferred to use #update_all for updating all records using + # a single query. def update(id = :all, attributes) if id.is_a?(Array) id.map.with_index { |one_id, idx| update(one_id, attributes[idx]) } elsif id == :all - to_a.each { |record| record.update(attributes) } + records.each { |record| record.update(attributes) } else + if ActiveRecord::Base === id + id = id.id + ActiveSupport::Deprecation.warn(<<-MSG.squish) + You are passing an instance of ActiveRecord::Base to `update`. + Please pass the id of the object by calling `.id`. + MSG + end object = find(id) object.update(attributes) object end end - # Destroys the records matching +conditions+ by instantiating each - # record and calling its +destroy+ method. Each object's callbacks are - # executed (including <tt>:dependent</tt> association options). Returns the - # collection of objects that were destroyed; each will be frozen, to + # Destroys the records by instantiating each + # record and calling its {#destroy}[rdoc-ref:Persistence#destroy] method. + # Each object's callbacks are executed (including <tt>:dependent</tt> association options). + # Returns the collection of objects that were destroyed; each will be frozen, to # reflect that no changes should be made (since they can't be persisted). # # Note: Instantiation, callback execution, and deletion of each @@ -403,31 +441,18 @@ module ActiveRecord # once. It generates at least one SQL +DELETE+ query per record (or # possibly more, to enforce your callbacks). If you want to delete many # rows quickly, without concern for their associations or callbacks, use - # +delete_all+ instead. - # - # ==== Parameters - # - # * +conditions+ - A string, array, or hash that specifies which records - # to destroy. If omitted, all records are destroyed. See the - # Conditions section in the introduction to ActiveRecord::Base for - # more information. + # #delete_all instead. # # ==== Examples # - # Person.destroy_all("last_login < '2004-04-04'") - # Person.destroy_all(status: "inactive") # Person.where(age: 0..18).destroy_all - def destroy_all(conditions = nil) - if conditions - where(conditions).destroy_all - else - to_a.each(&:destroy).tap { reset } - end + def destroy_all + records.each(&:destroy).tap { reset } end # Destroy an object (or multiple objects) that has the given id. The object is instantiated first, # therefore all callbacks and filters are fired off before the object is deleted. This method is - # less efficient than ActiveRecord#delete but allows cleanup methods and other actions to be run. + # less efficient than #delete but allows cleanup methods and other actions to be run. # # This essentially finds the object (or multiple objects) with the given id, creates a new object # from the attributes, and then calls destroy on it. @@ -452,56 +477,46 @@ module ActiveRecord end end - # Deletes the records matching +conditions+ without instantiating the records - # first, and hence not calling the +destroy+ method nor invoking callbacks. This - # is a single SQL DELETE statement that goes straight to the database, much more - # efficient than +destroy_all+. Be careful with relations though, in particular + # Deletes the records without instantiating the records + # first, and hence not calling the {#destroy}[rdoc-ref:Persistence#destroy] + # method nor invoking callbacks. + # This is a single SQL DELETE statement that goes straight to the database, much more + # efficient than #destroy_all. Be careful with relations though, in particular # <tt>:dependent</tt> rules defined on associations are not honored. Returns the # number of rows affected. # - # Post.delete_all("person_id = 5 AND (category = 'Something' OR category = 'Else')") - # Post.delete_all(["person_id = ? AND (category = ? OR category = ?)", 5, 'Something', 'Else']) # Post.where(person_id: 5).where(category: ['Something', 'Else']).delete_all # # Both calls delete the affected posts all at once with a single DELETE statement. # If you need to destroy dependent associations or call your <tt>before_*</tt> or - # +after_destroy+ callbacks, use the +destroy_all+ method instead. + # +after_destroy+ callbacks, use the #destroy_all method instead. # - # If an invalid method is supplied, +delete_all+ raises an ActiveRecord error: + # If an invalid method is supplied, #delete_all raises an ActiveRecordError: # # Post.limit(100).delete_all # # => ActiveRecord::ActiveRecordError: delete_all doesn't support limit - def delete_all(conditions = nil) - invalid_methods = INVALID_METHODS_FOR_DELETE_ALL.select { |method| - if MULTI_VALUE_METHODS.include?(method) - send("#{method}_values").any? - elsif SINGLE_VALUE_METHODS.include?(method) - send("#{method}_value") - elsif CLAUSE_METHODS.include?(method) - send("#{method}_clause").any? - end - } + def delete_all + invalid_methods = INVALID_METHODS_FOR_DELETE_ALL.select do |method| + value = get_value(method) + SINGLE_VALUE_METHODS.include?(method) ? value : value.any? + end if invalid_methods.any? raise ActiveRecordError.new("delete_all doesn't support #{invalid_methods.join(', ')}") end - if conditions - where(conditions).delete_all - else - stmt = Arel::DeleteManager.new - stmt.from(table) + stmt = Arel::DeleteManager.new + stmt.from(table) - if joins_values.any? - @klass.connection.join_to_delete(stmt, arel, table[primary_key]) - else - stmt.wheres = arel.constraints - end + if has_join_values? + @klass.connection.join_to_delete(stmt, arel, arel_attribute(primary_key)) + else + stmt.wheres = arel.constraints + end - affected = @klass.connection.delete(stmt, 'SQL', bound_attributes) + affected = @klass.connection.delete(stmt, "SQL", bound_attributes) - reset - affected - end + reset + affected end # Deletes the row with a primary key matching the +id+ argument, using a @@ -512,7 +527,7 @@ module ActiveRecord # You can delete multiple rows at once by passing an Array of <tt>id</tt>s. # # Note: Although it is often much faster than the alternative, - # <tt>#destroy</tt>, skipping callbacks might bypass business logic in + # #destroy, skipping callbacks might bypass business logic in # your application that ensures referential integrity or performs other # essential jobs. # @@ -533,8 +548,8 @@ module ActiveRecord # return value is the relation itself, not the records. # # Post.where(published: true).load # => #<ActiveRecord::Relation> - def load - exec_queries unless loaded? + def load(&block) + exec_queries(&block) unless loaded? self end @@ -548,7 +563,7 @@ module ActiveRecord def reset @last = @to_sql = @order_clause = @scope_for_create = @arel = @loaded = nil @should_eager_load = @join_dependency = nil - @records = [] + @records = [].freeze @offsets = {} self end @@ -559,19 +574,16 @@ module ActiveRecord # # => SELECT "users".* FROM "users" WHERE "users"."name" = 'Oscar' def to_sql @to_sql ||= begin - relation = self - connection = klass.connection - visitor = connection.visitor + relation = self if eager_loading? find_with_associations { |rel| relation = rel } end - binds = relation.bound_attributes - binds = connection.prepare_binds_for_database(binds) - binds.map! { |value| connection.quote(value) } - collect = visitor.accept(relation.arel.ast, Arel::Collectors::Bind.new) - collect.substitute_binds(binds).join + conn = klass.connection + conn.unprepared_statement { + conn.to_sql(relation.arel, relation.bound_attributes) + } end end @@ -602,89 +614,94 @@ module ActiveRecord includes_values & joins_values end - # +uniq+ and +uniq!+ are silently deprecated. +uniq_value+ delegates to +distinct_value+ - # to maintain backwards compatibility. Use +distinct_value+ instead. - def uniq_value - distinct_value - end - deprecate uniq_value: :distinct_value - # Compares two relations for equality. def ==(other) case other when Associations::CollectionProxy, AssociationRelation - self == other.to_a + self == other.records when Relation other.to_sql == to_sql when Array - to_a == other + records == other end end def pretty_print(q) - q.pp(self.to_a) + q.pp(records) end # Returns true if relation is blank. def blank? - to_a.blank? + records.blank? end def values - Hash[@values] + @values.dup end def inspect - entries = to_a.take([limit_value, 11].compact.min).map!(&:inspect) - entries[10] = '...' if entries.size == 11 + entries = records.take([limit_value, 11].compact.min).map!(&:inspect) + entries[10] = "..." if entries.size == 11 "#<#{self.class.name} [#{entries.join(', ')}]>" end - private - - def exec_queries - @records = eager_loading? ? find_with_associations : @klass.find_by_sql(arel, bound_attributes) + protected - preload = preload_values - preload += includes_values unless eager_loading? - preloader = build_preloader - preload.each do |associations| - preloader.preload @records, associations + def load_records(records) + @records = records.freeze + @loaded = true end - @records.each(&:readonly!) if readonly_value + private - @loaded = true - @records - end + def has_join_values? + joins_values.any? || left_outer_joins_values.any? + end - def build_preloader - ActiveRecord::Associations::Preloader.new - end + def exec_queries(&block) + @records = eager_loading? ? find_with_associations.freeze : @klass.find_by_sql(arel, bound_attributes, &block).freeze - def references_eager_loaded_tables? - joined_tables = arel.join_sources.map do |join| - if join.is_a?(Arel::Nodes::StringJoin) - tables_in_string(join.left) - else - [join.left.table_name, join.left.table_alias] + preload = preload_values + preload += includes_values unless eager_loading? + preloader = nil + preload.each do |associations| + preloader ||= build_preloader + preloader.preload @records, associations end + + @records.each(&:readonly!) if readonly_value + + @loaded = true + @records end - joined_tables += [table.name, table.table_alias] + def build_preloader + ActiveRecord::Associations::Preloader.new + end - # always convert table names to downcase as in Oracle quoted table names are in uppercase - joined_tables = joined_tables.flatten.compact.map(&:downcase).uniq + def references_eager_loaded_tables? + joined_tables = arel.join_sources.map do |join| + if join.is_a?(Arel::Nodes::StringJoin) + tables_in_string(join.left) + else + [join.left.table_name, join.left.table_alias] + end + end - (references_values - joined_tables).any? - end + joined_tables += [table.name, table.table_alias] - def tables_in_string(string) - return [] if string.blank? - # always convert table names to downcase as in Oracle quoted table names are in uppercase - # ignore raw_sql_ that is used by Oracle adapter as alias for limit/offset subqueries - string.scan(/([a-zA-Z_][.\w]+).?\./).flatten.map(&:downcase).uniq - ['raw_sql_'] - end + # always convert table names to downcase as in Oracle quoted table names are in uppercase + joined_tables = joined_tables.flatten.compact.map(&:downcase).uniq + + (references_values - joined_tables).any? + end + + def tables_in_string(string) + return [] if string.blank? + # always convert table names to downcase as in Oracle quoted table names are in uppercase + # ignore raw_sql_ that is used by Oracle adapter as alias for limit/offset subqueries + string.scan(/([a-zA-Z_][.\w]+).?\./).flatten.map(&:downcase).uniq - ["raw_sql_"] + end end end diff --git a/activerecord/lib/active_record/relation/batches.rb b/activerecord/lib/active_record/relation/batches.rb index e07580a563..4b2987ac6d 100644 --- a/activerecord/lib/active_record/relation/batches.rb +++ b/activerecord/lib/active_record/relation/batches.rb @@ -1,8 +1,12 @@ +require "active_record/relation/batches/batch_enumerator" + module ActiveRecord module Batches + ORDER_IGNORE_MESSAGE = "Scoped order is ignored, it's forced to be batch order." + # Looping through a collection of records from the database - # (using the +all+ method, for example) is very inefficient - # since it will try to instantiate all the objects at once. + # (using the Scoping::Named::ClassMethods.all method, for example) + # is very inefficient since it will try to instantiate all the objects at once. # # In that case, batch processing methods allow you to work # with the records in batches, thereby greatly reducing memory consumption. @@ -27,15 +31,22 @@ module ActiveRecord # # ==== Options # * <tt>:batch_size</tt> - Specifies the size of the batch. Default to 1000. - # * <tt>:begin_at</tt> - Specifies the primary key value to start from, inclusive of the value. - # * <tt>:end_at</tt> - Specifies the primary key value to end at, inclusive of the value. - # This is especially useful if you want multiple workers dealing with - # the same processing queue. You can make worker 1 handle all the records - # between id 0 and 10,000 and worker 2 handle from 10,000 and beyond - # (by setting the +:begin_at+ and +:end_at+ option on each worker). - # - # # Let's process for a batch of 2000 records, skipping the first 2000 rows - # Person.find_each(begin_at: 2000, batch_size: 2000) do |person| + # * <tt>:start</tt> - Specifies the primary key value to start from, inclusive of the value. + # * <tt>:finish</tt> - Specifies the primary key value to end at, inclusive of the value. + # * <tt>:error_on_ignore</tt> - Overrides the application config to specify if an error should be raised when + # an order is present in the relation. + # + # Limits are honored, and if present there is no requirement for the batch + # size, it can be less than, equal, or greater than the limit. + # + # The options +start+ and +finish+ are especially useful if you want + # multiple workers dealing with the same processing queue. You can make + # worker 1 handle all the records between id 1 and 9999 and worker 2 + # handle from 10000 and beyond by setting the +:start+ and +:finish+ + # option on each worker. + # + # # Let's process from record 10_000 on. + # Person.find_each(start: 10_000) do |person| # person.party_all_night! # end # @@ -44,24 +55,17 @@ module ActiveRecord # work. This also means that this method only works when the primary key is # orderable (e.g. an integer or string). # - # NOTE: You can't set the limit either, that's used to control - # the batch sizes. - def find_each(begin_at: nil, end_at: nil, batch_size: 1000, start: nil) - if start - begin_at = start - ActiveSupport::Deprecation.warn(<<-MSG.squish) - Passing `start` value to find_each is deprecated, and will be removed in Rails 5.1. - Please pass `begin_at` instead. - MSG - end + # NOTE: By its nature, batch processing is subject to race conditions if + # other processes are modifying the database. + def find_each(start: nil, finish: nil, batch_size: 1000, error_on_ignore: nil) if block_given? - find_in_batches(begin_at: begin_at, end_at: end_at, batch_size: batch_size) do |records| + find_in_batches(start: start, finish: finish, batch_size: batch_size, error_on_ignore: error_on_ignore) do |records| records.each { |record| yield record } end else - enum_for(:find_each, begin_at: begin_at, end_at: end_at, batch_size: batch_size) do + enum_for(:find_each, start: start, finish: finish, batch_size: batch_size, error_on_ignore: error_on_ignore) do relation = self - apply_limits(relation, begin_at, end_at).size + apply_limits(relation, start, finish).size end end end @@ -86,15 +90,22 @@ module ActiveRecord # # ==== Options # * <tt>:batch_size</tt> - Specifies the size of the batch. Default to 1000. - # * <tt>:begin_at</tt> - Specifies the primary key value to start from, inclusive of the value. - # * <tt>:end_at</tt> - Specifies the primary key value to end at, inclusive of the value. - # This is especially useful if you want multiple workers dealing with - # the same processing queue. You can make worker 1 handle all the records - # between id 0 and 10,000 and worker 2 handle from 10,000 and beyond - # (by setting the +:begin_at+ and +:end_at+ option on each worker). - # - # # Let's process the next 2000 records - # Person.find_in_batches(begin_at: 2000, batch_size: 2000) do |group| + # * <tt>:start</tt> - Specifies the primary key value to start from, inclusive of the value. + # * <tt>:finish</tt> - Specifies the primary key value to end at, inclusive of the value. + # * <tt>:error_on_ignore</tt> - Overrides the application config to specify if an error should be raised when + # an order is present in the relation. + # + # Limits are honored, and if present there is no requirement for the batch + # size, it can be less than, equal, or greater than the limit. + # + # The options +start+ and +finish+ are especially useful if you want + # multiple workers dealing with the same processing queue. You can make + # worker 1 handle all the records between id 1 and 9999 and worker 2 + # handle from 10000 and beyond by setting the +:start+ and +:finish+ + # option on each worker. + # + # # Let's process from record 10_000 on. + # Person.find_in_batches(start: 10_000) do |group| # group.each { |person| person.party_all_night! } # end # @@ -103,56 +114,159 @@ module ActiveRecord # work. This also means that this method only works when the primary key is # orderable (e.g. an integer or string). # - # NOTE: You can't set the limit either, that's used to control - # the batch sizes. - def find_in_batches(begin_at: nil, end_at: nil, batch_size: 1000, start: nil) - if start - begin_at = start - ActiveSupport::Deprecation.warn(<<-MSG.squish) - Passing `start` value to find_in_batches is deprecated, and will be removed in Rails 5.1. - Please pass `begin_at` instead. - MSG - end - + # NOTE: By its nature, batch processing is subject to race conditions if + # other processes are modifying the database. + def find_in_batches(start: nil, finish: nil, batch_size: 1000, error_on_ignore: nil) relation = self unless block_given? - return to_enum(:find_in_batches, begin_at: begin_at, end_at: end_at, batch_size: batch_size) do - total = apply_limits(relation, begin_at, end_at).size + return to_enum(:find_in_batches, start: start, finish: finish, batch_size: batch_size, error_on_ignore: error_on_ignore) do + total = apply_limits(relation, start, finish).size (total - 1).div(batch_size) + 1 end end - if logger && (arel.orders.present? || arel.taken.present?) - logger.warn("Scoped order and limit are ignored, it's forced to be batch order and batch size") + in_batches(of: batch_size, start: start, finish: finish, load: true, error_on_ignore: error_on_ignore) do |batch| + yield batch.to_a end + end - relation = relation.reorder(batch_order).limit(batch_size) - relation = apply_limits(relation, begin_at, end_at) - records = relation.to_a + # Yields ActiveRecord::Relation objects to work with a batch of records. + # + # Person.where("age > 21").in_batches do |relation| + # relation.delete_all + # sleep(10) # Throttle the delete queries + # end + # + # If you do not provide a block to #in_batches, it will return a + # BatchEnumerator which is enumerable. + # + # Person.in_batches.with_index do |relation, batch_index| + # puts "Processing relation ##{batch_index}" + # relation.each { |relation| relation.delete_all } + # end + # + # Examples of calling methods on the returned BatchEnumerator object: + # + # Person.in_batches.delete_all + # Person.in_batches.update_all(awesome: true) + # Person.in_batches.each_record(&:party_all_night!) + # + # ==== Options + # * <tt>:of</tt> - Specifies the size of the batch. Default to 1000. + # * <tt>:load</tt> - Specifies if the relation should be loaded. Default to false. + # * <tt>:start</tt> - Specifies the primary key value to start from, inclusive of the value. + # * <tt>:finish</tt> - Specifies the primary key value to end at, inclusive of the value. + # * <tt>:error_on_ignore</tt> - Overrides the application config to specify if an error should be raised when + # an order is present in the relation. + # + # Limits are honored, and if present there is no requirement for the batch + # size, it can be less than, equal, or greater than the limit. + # + # The options +start+ and +finish+ are especially useful if you want + # multiple workers dealing with the same processing queue. You can make + # worker 1 handle all the records between id 1 and 9999 and worker 2 + # handle from 10000 and beyond by setting the +:start+ and +:finish+ + # option on each worker. + # + # # Let's process from record 10_000 on. + # Person.in_batches(start: 10_000).update_all(awesome: true) + # + # An example of calling where query method on the relation: + # + # Person.in_batches.each do |relation| + # relation.update_all('age = age + 1') + # relation.where('age > 21').update_all(should_party: true) + # relation.where('age <= 21').delete_all + # end + # + # NOTE: If you are going to iterate through each record, you should call + # #each_record on the yielded BatchEnumerator: + # + # Person.in_batches.each_record(&:party_all_night!) + # + # NOTE: It's not possible to set the order. That is automatically set to + # ascending on the primary key ("id ASC") to make the batch ordering + # consistent. Therefore the primary key must be orderable, e.g an integer + # or a string. + # + # NOTE: By its nature, batch processing is subject to race conditions if + # other processes are modifying the database. + def in_batches(of: 1000, start: nil, finish: nil, load: false, error_on_ignore: nil) + relation = self + unless block_given? + return BatchEnumerator.new(of: of, start: start, finish: finish, relation: self) + end + + if arel.orders.present? + act_on_ignored_order(error_on_ignore) + end - while records.any? - records_size = records.size - primary_key_offset = records.last.id - raise "Primary key not included in the custom select clause" unless primary_key_offset + batch_limit = of + if limit_value + remaining = limit_value + batch_limit = remaining if remaining < batch_limit + end + + relation = relation.reorder(batch_order).limit(batch_limit) + relation = apply_limits(relation, start, finish) + batch_relation = relation + + loop do + if load + records = batch_relation.records + ids = records.map(&:id) + yielded_relation = where(primary_key => ids) + yielded_relation.load_records(records) + else + ids = batch_relation.pluck(primary_key) + yielded_relation = where(primary_key => ids) + end - yield records + break if ids.empty? - break if records_size < batch_size + primary_key_offset = ids.last + raise ArgumentError.new("Primary key not included in the custom select clause") unless primary_key_offset - records = relation.where(table[primary_key].gt(primary_key_offset)).to_a + yield yielded_relation + + break if ids.length < batch_limit + + if limit_value + remaining -= ids.length + + if remaining == 0 + # Saves a useless iteration when the limit is a multiple of the + # batch size. + break + elsif remaining < batch_limit + relation = relation.limit(remaining) + end + end + + batch_relation = relation.where(arel_attribute(primary_key).gt(primary_key_offset)) end end private - def apply_limits(relation, begin_at, end_at) - relation = relation.where(table[primary_key].gteq(begin_at)) if begin_at - relation = relation.where(table[primary_key].lteq(end_at)) if end_at - relation - end + def apply_limits(relation, start, finish) + relation = relation.where(arel_attribute(primary_key).gteq(start)) if start + relation = relation.where(arel_attribute(primary_key).lteq(finish)) if finish + relation + end - def batch_order - "#{quoted_table_name}.#{quoted_primary_key} ASC" - end + def batch_order + "#{quoted_table_name}.#{quoted_primary_key} ASC" + end + + def act_on_ignored_order(error_on_ignore) + raise_error = (error_on_ignore.nil? ? self.klass.error_on_ignored_order : error_on_ignore) + + if raise_error + raise ArgumentError.new(ORDER_IGNORE_MESSAGE) + elsif logger + logger.warn(ORDER_IGNORE_MESSAGE) + end + end end end diff --git a/activerecord/lib/active_record/relation/batches/batch_enumerator.rb b/activerecord/lib/active_record/relation/batches/batch_enumerator.rb new file mode 100644 index 0000000000..3555779ec2 --- /dev/null +++ b/activerecord/lib/active_record/relation/batches/batch_enumerator.rb @@ -0,0 +1,67 @@ +module ActiveRecord + module Batches + class BatchEnumerator + include Enumerable + + def initialize(of: 1000, start: nil, finish: nil, relation:) #:nodoc: + @of = of + @relation = relation + @start = start + @finish = finish + end + + # Looping through a collection of records from the database (using the + # +all+ method, for example) is very inefficient since it will try to + # instantiate all the objects at once. + # + # In that case, batch processing methods allow you to work with the + # records in batches, thereby greatly reducing memory consumption. + # + # Person.in_batches.each_record do |person| + # person.do_awesome_stuff + # end + # + # Person.where("age > 21").in_batches(of: 10).each_record do |person| + # person.party_all_night! + # end + # + # If you do not provide a block to #each_record, it will return an Enumerator + # for chaining with other methods: + # + # Person.in_batches.each_record.with_index do |person, index| + # person.award_trophy(index + 1) + # end + def each_record + return to_enum(:each_record) unless block_given? + + @relation.to_enum(:in_batches, of: @of, start: @start, finish: @finish, load: true).each do |relation| + relation.records.each { |record| yield record } + end + end + + # Delegates #delete_all, #update_all, #destroy_all methods to each batch. + # + # People.in_batches.delete_all + # People.where('age < 10').in_batches.destroy_all + # People.in_batches.update_all('age = age + 1') + [:delete_all, :update_all, :destroy_all].each do |method| + define_method(method) do |*args, &block| + @relation.to_enum(:in_batches, of: @of, start: @start, finish: @finish, load: false).each do |relation| + relation.send(method, *args, &block) + end + end + end + + # Yields an ActiveRecord::Relation object for each batch of records. + # + # Person.in_batches.each do |relation| + # relation.update_all(awesome: true) + # end + def each + enum = @relation.to_enum(:in_batches, of: @of, start: @start, finish: @finish, load: false) + return enum.each { |relation| yield relation } if block_given? + enum + end + end + end +end diff --git a/activerecord/lib/active_record/relation/calculations.rb b/activerecord/lib/active_record/relation/calculations.rb index df72ba7e9c..827688a663 100644 --- a/activerecord/lib/active_record/relation/calculations.rb +++ b/activerecord/lib/active_record/relation/calculations.rb @@ -14,33 +14,38 @@ module ActiveRecord # Person.distinct.count(:age) # # => counts the number of different age values # - # If +count+ is used with +group+, it returns a Hash whose keys represent the aggregated column, + # If #count is used with {Relation#group}[rdoc-ref:QueryMethods#group], + # it returns a Hash whose keys represent the aggregated column, # and the values are the respective amounts: # # Person.group(:city).count # # => { 'Rome' => 5, 'Paris' => 3 } # - # If +count+ is used with +group+ for multiple columns, it returns a Hash whose + # If #count is used with {Relation#group}[rdoc-ref:QueryMethods#group] for multiple columns, it returns a Hash whose # keys are an array containing the individual values of each column and the value - # of each key would be the +count+. + # of each key would be the #count. # # Article.group(:status, :category).count # # => {["draft", "business"]=>10, ["draft", "technology"]=>4, # ["published", "business"]=>0, ["published", "technology"]=>2} # - # If +count+ is used with +select+, it will count the selected columns: + # If #count is used with {Relation#select}[rdoc-ref:QueryMethods#select], it will count the selected columns: # # Person.select(:age).count # # => counts the number of different age values # - # Note: not all valid +select+ expressions are valid +count+ expressions. The specifics differ + # Note: not all valid {Relation#select}[rdoc-ref:QueryMethods#select] expressions are valid #count expressions. The specifics differ # between databases. In invalid cases, an error from the database is thrown. def count(column_name = nil) - calculate(:count, column_name) + if block_given? + to_a.count { |*block_args| yield(*block_args) } + else + calculate(:count, column_name) + end end # Calculates the average value on a given column. Returns +nil+ if there's - # no row. See +calculate+ for examples with options. + # no row. See #calculate for examples with options. # # Person.average(:age) # => 35.8 def average(column_name) @@ -49,7 +54,7 @@ module ActiveRecord # Calculates the minimum value on a given column. The value is returned # with the same data type of the column, or +nil+ if there's no row. See - # +calculate+ for examples with options. + # #calculate for examples with options. # # Person.minimum(:age) # => 7 def minimum(column_name) @@ -58,7 +63,7 @@ module ActiveRecord # Calculates the maximum value on a given column. The value is returned # with the same data type of the column, or +nil+ if there's no row. See - # +calculate+ for examples with options. + # #calculate for examples with options. # # Person.maximum(:age) # => 93 def maximum(column_name) @@ -66,59 +71,58 @@ module ActiveRecord end # Calculates the sum of values on a given column. The value is returned - # with the same data type of the column, 0 if there's no row. See - # +calculate+ for examples with options. + # with the same data type of the column, +0+ if there's no row. See + # #calculate for examples with options. # # Person.sum(:age) # => 4562 - def sum(*args) - return super if block_given? - calculate(:sum, *args) + def sum(column_name = nil, &block) + return super(&block) if block_given? + calculate(:sum, column_name) end - # This calculates aggregate values in the given column. Methods for count, sum, average, - # minimum, and maximum have been added as shortcuts. + # This calculates aggregate values in the given column. Methods for #count, #sum, #average, + # #minimum, and #maximum have been added as shortcuts. # - # There are two basic forms of output: + # Person.calculate(:count, :all) # The same as Person.count + # Person.average(:age) # SELECT AVG(age) FROM people... # - # * Single aggregate value: The single value is type cast to Fixnum for COUNT, Float - # for AVG, and the given column's type for everything else. + # # Selects the minimum age for any family without any minors + # Person.group(:last_name).having("min(age) > 17").minimum(:age) # - # * Grouped values: This returns an ordered hash of the values and groups them. It - # takes either a column name, or the name of a belongs_to association. + # Person.sum("2 * age") # - # values = Person.group('last_name').maximum(:age) - # puts values["Drake"] - # # => 43 + # There are two basic forms of output: # - # drake = Family.find_by(last_name: 'Drake') - # values = Person.group(:family).maximum(:age) # Person belongs_to :family - # puts values[drake] - # # => 43 + # * Single aggregate value: The single value is type cast to Integer for COUNT, Float + # for AVG, and the given column's type for everything else. # - # values.each do |family, max_age| - # ... - # end + # * Grouped values: This returns an ordered hash of the values and groups them. It + # takes either a column name, or the name of a belongs_to association. # - # Person.calculate(:count, :all) # The same as Person.count - # Person.average(:age) # SELECT AVG(age) FROM people... + # values = Person.group('last_name').maximum(:age) + # puts values["Drake"] + # # => 43 # - # # Selects the minimum age for any family without any minors - # Person.group(:last_name).having("min(age) > 17").minimum(:age) + # drake = Family.find_by(last_name: 'Drake') + # values = Person.group(:family).maximum(:age) # Person belongs_to :family + # puts values[drake] + # # => 43 # - # Person.sum("2 * age") + # values.each do |family, max_age| + # ... + # end def calculate(operation, column_name) - if column_name.is_a?(Symbol) && attribute_alias?(column_name) - column_name = attribute_alias(column_name) - end - if has_include?(column_name) - construct_relation_for_association_calculations.calculate(operation, column_name) + relation = construct_relation_for_association_calculations + relation = relation.distinct if operation.to_s.downcase == "count" + + relation.calculate(operation, column_name) else perform_calculation(operation, column_name) end end - # Use <tt>pluck</tt> as a shortcut to select one or more attributes without + # Use #pluck as a shortcut to select one or more attributes without # loading a bunch of records just to grab the attributes you want. # # Person.pluck(:name) @@ -127,7 +131,7 @@ module ActiveRecord # # Person.all.map(&:name) # - # Pluck returns an <tt>Array</tt> of attribute values type-casted to match + # Pluck returns an Array of attribute values type-casted to match # the plucked column names, if they can be deduced. Plucking an SQL fragment # returns String values by default. # @@ -139,7 +143,7 @@ module ActiveRecord # # SELECT people.id, people.name FROM people # # => [[1, 'David'], [2, 'Jeremy'], [3, 'Jose']] # - # Person.uniq.pluck(:role) + # Person.distinct.pluck(:role) # # SELECT DISTINCT role FROM people # # => ['admin', 'member', 'guest'] # @@ -151,19 +155,11 @@ module ActiveRecord # # SELECT DATEDIFF(updated_at, created_at) FROM people # # => ['0', '27761', '173'] # - # See also +ids+. + # See also #ids. # def pluck(*column_names) - column_names.map! do |column_name| - if column_name.is_a?(Symbol) && attribute_alias?(column_name) - attribute_alias(column_name) - else - column_name.to_s - end - end - - if loaded? && (column_names - @klass.column_names).empty? - return @records.pluck(*column_names) + if loaded? && (column_names.map(&:to_s) - @klass.attribute_names - @klass.attribute_aliases.keys).empty? + return records.pluck(*column_names) end if has_include?(column_names.first) @@ -171,7 +167,7 @@ module ActiveRecord else relation = spawn relation.select_values = column_names.map { |cn| - columns_hash.key?(cn) ? arel_table[cn] : cn + @klass.has_attribute?(cn) || @klass.attribute_alias?(cn) ? arel_attribute(cn) : cn } result = klass.connection.select_all(relation.arel, nil, bound_attributes) result.cast_values(klass.attribute_types) @@ -188,204 +184,196 @@ module ActiveRecord private - def has_include?(column_name) - eager_loading? || (includes_values.present? && column_name && column_name != :all) - end - - def perform_calculation(operation, column_name) - operation = operation.to_s.downcase + def has_include?(column_name) + eager_loading? || (includes_values.present? && column_name && column_name != :all) + end - # If #count is used with #distinct / #uniq it is considered distinct. (eg. relation.distinct.count) - distinct = self.distinct_value + def perform_calculation(operation, column_name) + operation = operation.to_s.downcase - if operation == "count" - column_name ||= select_for_count + # If #count is used with #distinct (i.e. `relation.distinct.count`) it is + # considered distinct. + distinct = self.distinct_value - unless arel.ast.grep(Arel::Nodes::OuterJoin).empty? - distinct = true + if operation == "count" + column_name ||= select_for_count + column_name = primary_key if column_name == :all && distinct + distinct = nil if column_name =~ /\s*DISTINCT[\s(]+/i end - column_name = primary_key if column_name == :all && distinct - distinct = nil if column_name =~ /\s*DISTINCT[\s(]+/i + if group_values.any? + execute_grouped_calculation(operation, column_name, distinct) + else + execute_simple_calculation(operation, column_name, distinct) + end end - if group_values.any? - execute_grouped_calculation(operation, column_name, distinct) - else - execute_simple_calculation(operation, column_name, distinct) - end - end + def aggregate_column(column_name) + return column_name if Arel::Expressions === column_name - def aggregate_column(column_name) - if @klass.column_names.include?(column_name.to_s) - Arel::Attribute.new(@klass.unscoped.table, column_name) - else - Arel.sql(column_name == :all ? "*" : column_name.to_s) + if @klass.has_attribute?(column_name.to_s) || @klass.attribute_alias?(column_name.to_s) + @klass.arel_attribute(column_name) + else + Arel.sql(column_name == :all ? "*" : column_name.to_s) + end end - end - - def operation_over_aggregate_column(column, operation, distinct) - operation == 'count' ? column.count(distinct) : column.send(operation) - end - - def execute_simple_calculation(operation, column_name, distinct) #:nodoc: - # PostgreSQL doesn't like ORDER BY when there are no GROUP BY - relation = unscope(:order) - column_alias = column_name + def operation_over_aggregate_column(column, operation, distinct) + operation == "count" ? column.count(distinct) : column.send(operation) + end - if operation == "count" && (relation.limit_value || relation.offset_value) - # Shortcut when limit is zero. - return 0 if relation.limit_value == 0 + def execute_simple_calculation(operation, column_name, distinct) #:nodoc: + column_alias = column_name - query_builder = build_count_subquery(relation, column_name, distinct) - else - column = aggregate_column(column_name) + if operation == "count" && (limit_value || offset_value) + # Shortcut when limit is zero. + return 0 if limit_value == 0 - select_value = operation_over_aggregate_column(column, operation, distinct) + query_builder = build_count_subquery(spawn, column_name, distinct) + else + # PostgreSQL doesn't like ORDER BY when there are no GROUP BY + relation = unscope(:order) - column_alias = select_value.alias - column_alias ||= @klass.connection.column_name_for_operation(operation, select_value) - relation.select_values = [select_value] + column = aggregate_column(column_name) - query_builder = relation.arel - end + select_value = operation_over_aggregate_column(column, operation, distinct) - result = @klass.connection.select_all(query_builder, nil, bound_attributes) - row = result.first - value = row && row.values.first - column = result.column_types.fetch(column_alias) do - type_for(column_name) - end + column_alias = select_value.alias + column_alias ||= @klass.connection.column_name_for_operation(operation, select_value) + relation.select_values = [select_value] - type_cast_calculated_value(value, column, operation) - end + query_builder = relation.arel + end - def execute_grouped_calculation(operation, column_name, distinct) #:nodoc: - group_attrs = group_values + result = @klass.connection.select_all(query_builder, nil, bound_attributes) + row = result.first + value = row && row.values.first + type = result.column_types.fetch(column_alias) do + type_for(column_name) + end - if group_attrs.first.respond_to?(:to_sym) - association = @klass._reflect_on_association(group_attrs.first) - associated = group_attrs.size == 1 && association && association.belongs_to? # only count belongs_to associations - group_fields = Array(associated ? association.foreign_key : group_attrs) - else - group_fields = group_attrs + type_cast_calculated_value(value, type, operation) end - group_aliases = group_fields.map { |field| - column_alias_for(field) - } - group_columns = group_aliases.zip(group_fields).map { |aliaz,field| - [aliaz, field] - } + def execute_grouped_calculation(operation, column_name, distinct) #:nodoc: + group_attrs = group_values - group = group_fields - - if operation == 'count' && column_name == :all - aggregate_alias = 'count_all' - else - aggregate_alias = column_alias_for([operation, column_name].join(' ')) - end - - select_values = [ - operation_over_aggregate_column( - aggregate_column(column_name), - operation, - distinct).as(aggregate_alias) - ] - select_values += select_values unless having_clause.empty? - - select_values.concat group_fields.zip(group_aliases).map { |field,aliaz| - if field.respond_to?(:as) - field.as(aliaz) + if group_attrs.first.respond_to?(:to_sym) + association = @klass._reflect_on_association(group_attrs.first) + associated = group_attrs.size == 1 && association && association.belongs_to? # only count belongs_to associations + group_fields = Array(associated ? association.foreign_key : group_attrs) else - "#{field} AS #{aliaz}" + group_fields = group_attrs end - } - - relation = except(:group) - relation.group_values = group - relation.select_values = select_values + group_fields = arel_columns(group_fields) - calculated_data = @klass.connection.select_all(relation, nil, relation.bound_attributes) + group_aliases = group_fields.map { |field| column_alias_for(field) } + group_columns = group_aliases.zip(group_fields) - if association - key_ids = calculated_data.collect { |row| row[group_aliases.first] } - key_records = association.klass.base_class.where(association.klass.base_class.primary_key => key_ids) - key_records = Hash[key_records.map { |r| [r.id, r] }] - end + if operation == "count" && column_name == :all + aggregate_alias = "count_all" + else + aggregate_alias = column_alias_for([operation, column_name].join(" ")) + end - Hash[calculated_data.map do |row| - key = group_columns.map { |aliaz, col_name| - column = calculated_data.column_types.fetch(aliaz) do - type_for(col_name) + select_values = [ + operation_over_aggregate_column( + aggregate_column(column_name), + operation, + distinct).as(aggregate_alias) + ] + select_values += select_values unless having_clause.empty? + + select_values.concat group_columns.map { |aliaz, field| + if field.respond_to?(:as) + field.as(aliaz) + else + "#{field} AS #{aliaz}" end - type_cast_calculated_value(row[aliaz], column) } - key = key.first if key.size == 1 - key = key_records[key] if associated - column_type = calculated_data.column_types.fetch(aggregate_alias) { type_for(column_name) } - [key, type_cast_calculated_value(row[aggregate_alias], column_type, operation)] - end] - end + relation = except(:group) + relation.group_values = group_fields + relation.select_values = select_values - # Converts the given keys to the value that the database adapter returns as - # a usable column name: - # - # column_alias_for("users.id") # => "users_id" - # column_alias_for("sum(id)") # => "sum_id" - # 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) - if keys.respond_to? :name - keys = "#{keys.relation.name}.#{keys.name}" + calculated_data = @klass.connection.select_all(relation, nil, relation.bound_attributes) + + if association + key_ids = calculated_data.collect { |row| row[group_aliases.first] } + key_records = association.klass.base_class.where(association.klass.base_class.primary_key => key_ids) + key_records = Hash[key_records.map { |r| [r.id, r] }] + end + + Hash[calculated_data.map do |row| + key = group_columns.map { |aliaz, col_name| + type = type_for(col_name) do + calculated_data.column_types.fetch(aliaz, Type.default_value) + end + type_cast_calculated_value(row[aliaz], type) + } + key = key.first if key.size == 1 + key = key_records[key] if associated + + type = calculated_data.column_types.fetch(aggregate_alias) { type_for(column_name) } + [key, type_cast_calculated_value(row[aggregate_alias], type, operation)] + end] end - table_name = keys.to_s.downcase - table_name.gsub!(/\*/, 'all') - table_name.gsub!(/\W+/, ' ') - table_name.strip! - table_name.gsub!(/ +/, '_') + # Converts the given keys to the value that the database adapter returns as + # a usable column name: + # + # column_alias_for("users.id") # => "users_id" + # column_alias_for("sum(id)") # => "sum_id" + # column_alias_for("count(distinct users.id)") # => "count_distinct_users_id" + # column_alias_for("count(*)") # => "count_all" + def column_alias_for(keys) + if keys.respond_to? :name + keys = "#{keys.relation.name}.#{keys.name}" + end - @klass.connection.table_alias_for(table_name) - end + table_name = keys.to_s.downcase + table_name.gsub!(/\*/, "all") + table_name.gsub!(/\W+/, " ") + table_name.strip! + table_name.gsub!(/ +/, "_") - def type_for(field) - field_name = field.respond_to?(:name) ? field.name.to_s : field.to_s.split('.').last - @klass.type_for_attribute(field_name) - end + @klass.connection.table_alias_for(table_name) + end + + def type_for(field, &block) + field_name = field.respond_to?(:name) ? field.name.to_s : field.to_s.split(".").last + @klass.type_for_attribute(field_name, &block) + end - def type_cast_calculated_value(value, type, operation = nil) - case operation - when 'count' then value.to_i - when 'sum' then type.deserialize(value || 0) - when 'average' then value.respond_to?(:to_d) ? value.to_d : value + def type_cast_calculated_value(value, type, operation = nil) + case operation + when "count" then value.to_i + when "sum" then type.deserialize(value || 0) + when "average" then value.respond_to?(:to_d) ? value.to_d : value else type.deserialize(value) + end end - end - # TODO: refactor to allow non-string `select_values` (eg. Arel nodes). - def select_for_count - if select_values.present? - select_values.join(", ") - else - :all + def select_for_count + if select_values.present? + return select_values.first if select_values.one? + select_values.join(", ") + else + :all + end end - end - def build_count_subquery(relation, column_name, distinct) - column_alias = Arel.sql('count_column') - subquery_alias = Arel.sql('subquery_for_count') + def build_count_subquery(relation, column_name, distinct) + column_alias = Arel.sql("count_column") + subquery_alias = Arel.sql("subquery_for_count") - aliased_column = aggregate_column(column_name == :all ? 1 : column_name).as(column_alias) - relation.select_values = [aliased_column] - subquery = relation.arel.as(subquery_alias) + aliased_column = aggregate_column(column_name == :all ? 1 : column_name).as(column_alias) + relation.select_values = [aliased_column] + subquery = relation.arel.as(subquery_alias) - sm = Arel::SelectManager.new relation.engine - select_value = operation_over_aggregate_column(column_alias, 'count', distinct) - sm.project(select_value).from(subquery) - end + sm = Arel::SelectManager.new relation.engine + select_value = operation_over_aggregate_column(column_alias, "count", distinct) + sm.project(select_value).from(subquery) + end end end diff --git a/activerecord/lib/active_record/relation/delegation.rb b/activerecord/lib/active_record/relation/delegation.rb index 86f2c30168..3c1dea8c6c 100644 --- a/activerecord/lib/active_record/relation/delegation.rb +++ b/activerecord/lib/active_record/relation/delegation.rb @@ -1,14 +1,11 @@ -require 'set' -require 'active_support/concern' - module ActiveRecord module Delegation # :nodoc: - module DelegateCache - def relation_delegate_class(klass) # :nodoc: + module DelegateCache # :nodoc: + def relation_delegate_class(klass) @relation_delegate_cache[klass] end - def initialize_relation_delegate_cache # :nodoc: + def initialize_relation_delegate_cache @relation_delegate_cache = cache = {} [ ActiveRecord::Relation, @@ -18,7 +15,10 @@ module ActiveRecord delegate = Class.new(klass) { include ClassSpecificRelation } - const_set klass.name.gsub('::', '_'), delegate + mangled_name = klass.name.gsub("::".freeze, "_".freeze) + const_set mangled_name, delegate + private_constant mangled_name + cache[klass] = delegate end end @@ -36,16 +36,12 @@ module ActiveRecord # may vary depending on the klass of a relation, so we create a subclass of Relation # for each different klass, and the delegations are compiled into that subclass only. - BLACKLISTED_ARRAY_METHODS = [ - :compact!, :flatten!, :reject!, :reverse!, :rotate!, :map!, - :shuffle!, :slice!, :sort!, :sort_by!, :delete_if, - :keep_if, :pop, :shift, :delete_at, :select! - ].to_set # :nodoc: - - delegate :to_xml, :to_yaml, :length, :collect, :map, :each, :all?, :include?, :to_ary, :join, to: :to_a + delegate :to_xml, :encode_with, :length, :collect, :map, :each, :all?, :include?, :to_ary, :join, + :[], :&, :|, :+, :-, :sample, :reverse, :compact, :in_groups, :in_groups_of, + :shuffle, :split, :index, to: :records delegate :table_name, :quoted_table_name, :primary_key, :quoted_primary_key, - :connection, :columns_hash, :to => :klass + :connection, :columns_hash, to: :klass module ClassSpecificRelation # :nodoc: extend ActiveSupport::Concern @@ -63,7 +59,7 @@ module ActiveRecord @delegation_mutex.synchronize do return if method_defined?(method) - if method.to_s =~ /\A[a-zA-Z_]\w*[!?]?\z/ + if /\A[a-zA-Z_]\w*[!?]?\z/.match?(method) module_eval <<-RUBY, __FILE__, __LINE__ + 1 def #{method}(*args, &block) scoping { @klass.#{method}(*args, &block) } @@ -85,19 +81,19 @@ module ActiveRecord end end - protected + private - def method_missing(method, *args, &block) - if @klass.respond_to?(method) - self.class.delegate_to_scoped_klass(method) - scoping { @klass.public_send(method, *args, &block) } - elsif arel.respond_to?(method) - self.class.delegate method, :to => :arel - arel.public_send(method, *args, &block) - else - super + def method_missing(method, *args, &block) + if @klass.respond_to?(method) + self.class.delegate_to_scoped_klass(method) + scoping { @klass.public_send(method, *args, &block) } + elsif arel.respond_to?(method) + self.class.delegate method, to: :arel + arel.public_send(method, *args, &block) + else + super + end end - end end module ClassMethods # :nodoc: @@ -107,33 +103,26 @@ module ActiveRecord private - def relation_class_for(klass) - klass.relation_delegate_class(self) - end + def relation_class_for(klass) + klass.relation_delegate_class(self) + end end - def respond_to?(method, include_private = false) + def respond_to_missing?(method, include_private = false) super || @klass.respond_to?(method, include_private) || - array_delegable?(method) || arel.respond_to?(method, include_private) end protected - def array_delegable?(method) - Array.method_defined?(method) && BLACKLISTED_ARRAY_METHODS.exclude?(method) - end - - def method_missing(method, *args, &block) - if @klass.respond_to?(method) - scoping { @klass.public_send(method, *args, &block) } - elsif array_delegable?(method) - to_a.public_send(method, *args, &block) - elsif arel.respond_to?(method) - arel.public_send(method, *args, &block) - else - super + def method_missing(method, *args, &block) + if @klass.respond_to?(method) + scoping { @klass.public_send(method, *args, &block) } + elsif arel.respond_to?(method) + arel.public_send(method, *args, &block) + else + super + end end - end end end diff --git a/activerecord/lib/active_record/relation/finder_methods.rb b/activerecord/lib/active_record/relation/finder_methods.rb index 9fef55adea..dd92f40dee 100644 --- a/activerecord/lib/active_record/relation/finder_methods.rb +++ b/activerecord/lib/active_record/relation/finder_methods.rb @@ -1,8 +1,8 @@ -require 'active_support/core_ext/string/filters' +require "active_support/core_ext/string/filters" module ActiveRecord module FinderMethods - ONE_AS_ONE = '1 AS one' + ONE_AS_ONE = "1 AS one" # Find by id - This can either be a specific id (1), a list of ids (1, 5, 6), or an array of ids ([5, 6, 10]). # If one or more records can not be found for the requested ids, then RecordNotFound will be raised. If the primary key @@ -17,7 +17,7 @@ module ActiveRecord # Person.where("administrator = 1").order("created_on DESC").find(1) # # NOTE: The returned records may not be in the same order as the ids you - # provide since database rows are unordered. You'd need to provide an explicit <tt>order</tt> + # provide since database rows are unordered. You'd need to provide an explicit QueryMethods#order # option if you want the results are sorted. # # ==== Find with lock @@ -34,7 +34,7 @@ module ActiveRecord # person.save! # end # - # ==== Variations of +find+ + # ==== Variations of #find # # Person.where(name: 'Spartacus', rating: 4) # # returns a chainable list (which can be empty). @@ -42,13 +42,13 @@ module ActiveRecord # Person.find_by(name: 'Spartacus', rating: 4) # # returns the first item or nil. # - # Person.where(name: 'Spartacus', rating: 4).first_or_initialize + # Person.find_or_initialize_by(name: 'Spartacus', rating: 4) # # returns the first item or returns a new instance (requires you call .save to persist against the database). # - # Person.where(name: 'Spartacus', rating: 4).first_or_create + # Person.find_or_create_by(name: 'Spartacus', rating: 4) # # returns the first item or creates it and returns it. # - # ==== Alternatives for +find+ + # ==== Alternatives for #find # # Person.where(name: 'Spartacus', rating: 4).exists?(conditions = :none) # # returns a boolean indicating if any record with the given conditions exist. @@ -76,16 +76,17 @@ module ActiveRecord # Post.find_by "published_at < ?", 2.weeks.ago def find_by(arg, *args) where(arg, *args).take - rescue RangeError + rescue ::RangeError nil end - # Like <tt>find_by</tt>, except that if no record is found, raises - # an <tt>ActiveRecord::RecordNotFound</tt> error. + # Like #find_by, except that if no record is found, raises + # an ActiveRecord::RecordNotFound error. def find_by!(arg, *args) where(arg, *args).take! - rescue RangeError - raise RecordNotFound, "Couldn't find #{@klass.name} with an out of range value" + rescue ::RangeError + raise RecordNotFound.new("Couldn't find #{@klass.name} with an out of range value", + @klass.name) end # Gives a record (or N records if a parameter is supplied) without any implied @@ -96,13 +97,13 @@ module ActiveRecord # Person.take(5) # returns 5 objects fetched by SELECT * FROM people LIMIT 5 # Person.where(["name LIKE '%?'", name]).take def take(limit = nil) - limit ? limit(limit).to_a : find_take + limit ? find_take_with_limit(limit) : find_take end - # Same as +take+ but raises <tt>ActiveRecord::RecordNotFound</tt> if no record - # is found. Note that <tt>take!</tt> accepts no arguments. + # Same as #take but raises ActiveRecord::RecordNotFound if no record + # is found. Note that #take! accepts no arguments. def take! - take or raise RecordNotFound.new("Couldn't find #{@klass.name} with [#{arel.where_sql(@klass.arel_engine)}]") + take || raise_record_not_found_exception! end # Find the first record (or first N records if a parameter is supplied). @@ -116,16 +117,16 @@ module ActiveRecord # def first(limit = nil) if limit - find_nth_with_limit(offset_index, limit) + find_nth_with_limit(0, limit) else - find_nth(0, offset_index) + find_nth 0 end end - # Same as +first+ but raises <tt>ActiveRecord::RecordNotFound</tt> if no record - # is found. Note that <tt>first!</tt> accepts no arguments. + # Same as #first but raises ActiveRecord::RecordNotFound if no record + # is found. Note that #first! accepts no arguments. def first! - find_nth! 0 + first || raise_record_not_found_exception! end # Find the last record (or last N records if a parameter is supplied). @@ -144,21 +145,19 @@ module ActiveRecord # # [#<Person id:4>, #<Person id:3>, #<Person id:2>] def last(limit = nil) - if limit - if order_values.empty? && primary_key - order(arel_table[primary_key].desc).limit(limit).reverse - else - to_a.last(limit) - end - else - find_last - end + return find_last(limit) if loaded? || limit_value + + result = limit(limit || 1) + result.order!(arel_attribute(primary_key)) if order_values.empty? && primary_key + result = result.reverse_order! + + limit ? result.reverse : result.first end - # Same as +last+ but raises <tt>ActiveRecord::RecordNotFound</tt> if no record - # is found. Note that <tt>last!</tt> accepts no arguments. + # Same as #last but raises ActiveRecord::RecordNotFound if no record + # is found. Note that #last! accepts no arguments. def last! - last or raise RecordNotFound.new("Couldn't find #{@klass.name} with [#{arel.where_sql(@klass.arel_engine)}]") + last || raise_record_not_found_exception! end # Find the second record. @@ -168,13 +167,13 @@ module ActiveRecord # Person.offset(3).second # returns the second object from OFFSET 3 (which is OFFSET 4) # Person.where(["user_name = :u", { u: user_name }]).second def second - find_nth(1, offset_index) + find_nth 1 end - # Same as +second+ but raises <tt>ActiveRecord::RecordNotFound</tt> if no record + # Same as #second but raises ActiveRecord::RecordNotFound if no record # is found. def second! - find_nth! 1 + second || raise_record_not_found_exception! end # Find the third record. @@ -184,13 +183,13 @@ module ActiveRecord # Person.offset(3).third # returns the third object from OFFSET 3 (which is OFFSET 5) # Person.where(["user_name = :u", { u: user_name }]).third def third - find_nth(2, offset_index) + find_nth 2 end - # Same as +third+ but raises <tt>ActiveRecord::RecordNotFound</tt> if no record + # Same as #third but raises ActiveRecord::RecordNotFound if no record # is found. def third! - find_nth! 2 + third || raise_record_not_found_exception! end # Find the fourth record. @@ -200,13 +199,13 @@ module ActiveRecord # Person.offset(3).fourth # returns the fourth object from OFFSET 3 (which is OFFSET 6) # Person.where(["user_name = :u", { u: user_name }]).fourth def fourth - find_nth(3, offset_index) + find_nth 3 end - # Same as +fourth+ but raises <tt>ActiveRecord::RecordNotFound</tt> if no record + # Same as #fourth but raises ActiveRecord::RecordNotFound if no record # is found. def fourth! - find_nth! 3 + fourth || raise_record_not_found_exception! end # Find the fifth record. @@ -216,13 +215,13 @@ module ActiveRecord # Person.offset(3).fifth # returns the fifth object from OFFSET 3 (which is OFFSET 7) # Person.where(["user_name = :u", { u: user_name }]).fifth def fifth - find_nth(4, offset_index) + find_nth 4 end - # Same as +fifth+ but raises <tt>ActiveRecord::RecordNotFound</tt> if no record + # Same as #fifth but raises ActiveRecord::RecordNotFound if no record # is found. def fifth! - find_nth! 4 + fifth || raise_record_not_found_exception! end # Find the forty-second record. Also known as accessing "the reddit". @@ -232,17 +231,49 @@ module ActiveRecord # Person.offset(3).forty_two # returns the forty-second object from OFFSET 3 (which is OFFSET 44) # Person.where(["user_name = :u", { u: user_name }]).forty_two def forty_two - find_nth(41, offset_index) + find_nth 41 end - # Same as +forty_two+ but raises <tt>ActiveRecord::RecordNotFound</tt> if no record + # Same as #forty_two but raises ActiveRecord::RecordNotFound if no record # is found. def forty_two! - find_nth! 41 + forty_two || raise_record_not_found_exception! + end + + # Find the third-to-last record. + # If no order is defined it will order by primary key. + # + # Person.third_to_last # returns the third-to-last object fetched by SELECT * FROM people + # Person.offset(3).third_to_last # returns the third-to-last object from OFFSET 3 + # Person.where(["user_name = :u", { u: user_name }]).third_to_last + def third_to_last + find_nth_from_last 3 + end + + # Same as #third_to_last but raises ActiveRecord::RecordNotFound if no record + # is found. + def third_to_last! + third_to_last || raise_record_not_found_exception! + end + + # Find the second-to-last record. + # If no order is defined it will order by primary key. + # + # Person.second_to_last # returns the second-to-last object fetched by SELECT * FROM people + # Person.offset(3).second_to_last # returns the second-to-last object from OFFSET 3 + # Person.where(["user_name = :u", { u: user_name }]).second_to_last + def second_to_last + find_nth_from_last 2 end - # Returns +true+ if a record exists in the table that matches the +id+ or - # conditions given, or +false+ otherwise. The argument can take six forms: + # Same as #second_to_last but raises ActiveRecord::RecordNotFound if no record + # is found. + def second_to_last! + second_to_last || raise_record_not_found_exception! + end + + # Returns true if a record exists in the table that matches the +id+ or + # conditions given, or false otherwise. The argument can take six forms: # # * Integer - Finds the record with this primary key. # * String - Finds the record with a primary key corresponding to this @@ -255,7 +286,7 @@ module ActiveRecord # * No args - Returns +false+ if the table is empty, +true+ otherwise. # # For more information about specifying conditions as a hash or array, - # see the Conditions section in the introduction to <tt>ActiveRecord::Base</tt>. + # see the Conditions section in the introduction to ActiveRecord::Base. # # Note: You can't pass in a condition as a string (like <tt>name = # 'Jamie'</tt>), since it would be sanitized and then queried against @@ -273,16 +304,16 @@ module ActiveRecord conditions = conditions.id ActiveSupport::Deprecation.warn(<<-MSG.squish) You are passing an instance of ActiveRecord::Base to `exists?`. - Please pass the id of the object by calling `.id` + Please pass the id of the object by calling `.id`. MSG end return false if !conditions - relation = apply_join_dependency(self, construct_join_dependency) + relation = apply_join_dependency(self, construct_join_dependency(eager_loading: false)) return false if ActiveRecord::NullRelation === relation - relation = relation.except(:select, :order).select(ONE_AS_ONE).limit(1) + relation = relation.except(:select, :distinct).select(ONE_AS_ONE).limit(1) case conditions when Array, Hash @@ -294,217 +325,247 @@ module ActiveRecord end connection.select_value(relation, "#{name} Exists", relation.bound_attributes) ? true : false + rescue ::RangeError + false end # This method is called whenever no records are found with either a single - # id or multiple ids and raises a +ActiveRecord::RecordNotFound+ exception. + # id or multiple ids and raises an ActiveRecord::RecordNotFound exception. # # The error message is different depending on whether a single id or # multiple ids are provided. If multiple ids are provided, then the number # of results obtained should be provided in the +result_size+ argument and # the expected number of results should be provided in the +expected_size+ # argument. - def raise_record_not_found_exception!(ids, result_size, expected_size) #:nodoc: + def raise_record_not_found_exception!(ids = nil, result_size = nil, expected_size = nil, key = primary_key) # :nodoc: conditions = arel.where_sql(@klass.arel_engine) conditions = " [#{conditions}]" if conditions - - if Array(ids).size == 1 - error = "Couldn't find #{@klass.name} with '#{primary_key}'=#{ids}#{conditions}" + name = @klass.name + + if ids.nil? + error = "Couldn't find #{name}" + error << " with#{conditions}" if conditions + raise RecordNotFound.new(error, name) + elsif Array(ids).size == 1 + error = "Couldn't find #{name} with '#{key}'=#{ids}#{conditions}" + raise RecordNotFound.new(error, name, key, ids) else - error = "Couldn't find all #{@klass.name.pluralize} with '#{primary_key}': " + error = "Couldn't find all #{name.pluralize} with '#{key}': " error << "(#{ids.join(", ")})#{conditions} (found #{result_size} results, but was looking for #{expected_size})" - end - raise RecordNotFound, error + raise RecordNotFound.new(error, name, primary_key, ids) + end end private - def offset_index - offset_value || 0 - end + def offset_index + offset_value || 0 + end - def find_with_associations - # NOTE: the JoinDependency constructed here needs to know about - # any joins already present in `self`, so pass them in - # - # failing to do so means that in cases like activerecord/test/cases/associations/inner_join_association_test.rb:136 - # incorrect SQL is generated. In that case, the join dependency for - # SpecialCategorizations is constructed without knowledge of the - # preexisting join in joins_values to categorizations (by way of - # the `has_many :through` for categories). - # - join_dependency = construct_join_dependency(joins_values) - - aliases = join_dependency.aliases - relation = select aliases.columns - relation = apply_join_dependency(relation, join_dependency) - - if block_given? - yield relation - else - if ActiveRecord::NullRelation === relation - [] + def find_with_associations + # NOTE: the JoinDependency constructed here needs to know about + # any joins already present in `self`, so pass them in + # + # failing to do so means that in cases like activerecord/test/cases/associations/inner_join_association_test.rb:136 + # incorrect SQL is generated. In that case, the join dependency for + # SpecialCategorizations is constructed without knowledge of the + # preexisting join in joins_values to categorizations (by way of + # the `has_many :through` for categories). + # + join_dependency = construct_join_dependency(joins_values) + + aliases = join_dependency.aliases + relation = select aliases.columns + relation = apply_join_dependency(relation, join_dependency) + + if block_given? + yield relation else - arel = relation.arel - rows = connection.select_all(arel, 'SQL', relation.bound_attributes) - join_dependency.instantiate(rows, aliases) + if ActiveRecord::NullRelation === relation + [] + else + arel = relation.arel + rows = connection.select_all(arel, "SQL", relation.bound_attributes) + join_dependency.instantiate(rows, aliases) + end end end - end - def construct_join_dependency(joins = []) - including = eager_load_values + includes_values - ActiveRecord::Associations::JoinDependency.new(@klass, including, joins) - end + def construct_join_dependency(joins = [], eager_loading: true) + including = eager_load_values + includes_values + ActiveRecord::Associations::JoinDependency.new(@klass, including, joins, eager_loading: eager_loading) + end - def construct_relation_for_association_calculations - from = arel.froms.first - if Arel::Table === from + def construct_relation_for_association_calculations apply_join_dependency(self, construct_join_dependency(joins_values)) - else - # FIXME: as far as I can tell, `from` will always be an Arel::Table. - # There are no tests that test this branch, but presumably it's - # possible for `from` to be a list? - apply_join_dependency(self, construct_join_dependency(from)) end - end - def apply_join_dependency(relation, join_dependency) - relation = relation.except(:includes, :eager_load, :preload) - relation = relation.joins join_dependency + def apply_join_dependency(relation, join_dependency) + relation = relation.except(:includes, :eager_load, :preload) + relation = relation.joins join_dependency - if using_limitable_reflections?(join_dependency.reflections) - relation - else - if relation.limit_value - limited_ids = limited_ids_for(relation) - limited_ids.empty? ? relation.none! : relation.where!(primary_key => limited_ids) + if using_limitable_reflections?(join_dependency.reflections) + relation + else + if relation.limit_value + limited_ids = limited_ids_for(relation) + limited_ids.empty? ? relation.none! : relation.where!(primary_key => limited_ids) + end + relation.except(:limit, :offset) end - relation.except(:limit, :offset) end - end - def limited_ids_for(relation) - values = @klass.connection.columns_for_distinct( - "#{quoted_table_name}.#{quoted_primary_key}", relation.order_values) + def limited_ids_for(relation) + values = @klass.connection.columns_for_distinct( + "#{quoted_table_name}.#{quoted_primary_key}", relation.order_values) - relation = relation.except(:select).select(values).distinct! - arel = relation.arel + relation = relation.except(:select).select(values).distinct! + arel = relation.arel - id_rows = @klass.connection.select_all(arel, 'SQL', relation.bound_attributes) - id_rows.map {|row| row[primary_key]} - end + id_rows = @klass.connection.select_all(arel, "SQL", relation.bound_attributes) + id_rows.map { |row| row[primary_key] } + end - def using_limitable_reflections?(reflections) - reflections.none?(&:collection?) - end + def using_limitable_reflections?(reflections) + reflections.none?(&:collection?) + end - protected + private - def find_with_ids(*ids) - raise UnknownPrimaryKey.new(@klass) if primary_key.nil? + def find_with_ids(*ids) + raise UnknownPrimaryKey.new(@klass) if primary_key.nil? - expects_array = ids.first.kind_of?(Array) - return ids.first if expects_array && ids.first.empty? + expects_array = ids.first.kind_of?(Array) + return ids.first if expects_array && ids.first.empty? - ids = ids.flatten.compact.uniq + ids = ids.flatten.compact.uniq - case ids.size - when 0 - raise RecordNotFound, "Couldn't find #{@klass.name} without an ID" - when 1 - result = find_one(ids.first) - expects_array ? [ result ] : result - else - find_some(ids) - end - rescue RangeError - raise RecordNotFound, "Couldn't find #{@klass.name} with an out of range ID" - end + case ids.size + when 0 + raise RecordNotFound, "Couldn't find #{@klass.name} without an ID" + when 1 + result = find_one(ids.first) + expects_array ? [ result ] : result + else + find_some(ids) + end + rescue ::RangeError + raise RecordNotFound, "Couldn't find #{@klass.name} with an out of range ID" + end - def find_one(id) - if ActiveRecord::Base === id - id = id.id - ActiveSupport::Deprecation.warn(<<-MSG.squish) - You are passing an instance of ActiveRecord::Base to `find`. - Please pass the id of the object by calling `.id` - MSG - end + def find_one(id) + if ActiveRecord::Base === id + id = id.id + ActiveSupport::Deprecation.warn(<<-MSG.squish) + You are passing an instance of ActiveRecord::Base to `find`. + Please pass the id of the object by calling `.id`. + MSG + end - relation = where(primary_key => id) - record = relation.take + relation = where(primary_key => id) + record = relation.take - raise_record_not_found_exception!(id, 0, 1) unless record + raise_record_not_found_exception!(id, 0, 1) unless record - record - end + record + end - def find_some(ids) - result = where(primary_key => ids).to_a + def find_some(ids) + return find_some_ordered(ids) unless order_values.present? - expected_size = - if limit_value && ids.size > limit_value - limit_value - else - ids.size + result = where(primary_key => ids).to_a + + expected_size = + if limit_value && ids.size > limit_value + limit_value + else + ids.size + end + + # 11 ids with limit 3, offset 9 should give 2 results. + if offset_value && (ids.size - offset_value < expected_size) + expected_size = ids.size - offset_value + end + + if result.size == expected_size + result + else + raise_record_not_found_exception!(ids, result.size, expected_size) + end end - # 11 ids with limit 3, offset 9 should give 2 results. - if offset_value && (ids.size - offset_value < expected_size) - expected_size = ids.size - offset_value - end + def find_some_ordered(ids) + ids = ids.slice(offset_value || 0, limit_value || ids.size) || [] - if result.size == expected_size - result - else - raise_record_not_found_exception!(ids, result.size, expected_size) - end - end + result = except(:limit, :offset).where(primary_key => ids).records - def find_take - if loaded? - @records.first - else - @take ||= limit(1).to_a.first - end - end + if result.size == ids.size + pk_type = @klass.type_for_attribute(primary_key) - def find_nth(index, offset) - if loaded? - @records[index] - else - offset += index - @offsets[offset] ||= find_nth_with_limit(offset, 1).first - end - end + records_by_id = result.index_by(&:id) + ids.map { |id| records_by_id.fetch(pk_type.cast(id)) } + else + raise_record_not_found_exception!(ids, result.size, ids.size) + end + end - def find_nth!(index) - find_nth(index, offset_index) or raise RecordNotFound.new("Couldn't find #{@klass.name} with [#{arel.where_sql(@klass.arel_engine)}]") - end + def find_take + if loaded? + records.first + else + @take ||= limit(1).records.first + end + end - def find_nth_with_limit(offset, limit) - relation = if order_values.empty? && primary_key - order(arel_table[primary_key].asc) - else - self - end + def find_take_with_limit(limit) + if loaded? + records.take(limit) + else + limit(limit).to_a + end + end - relation = relation.offset(offset) unless offset.zero? - relation.limit(limit).to_a - end + def find_nth(index) + @offsets[offset_index + index] ||= find_nth_with_limit(index, 1).first + end - def find_last - if loaded? - @records.last - else - @last ||= - if limit_value - to_a.last + def find_nth_with_limit(index, limit) + if loaded? + records[index, limit] || [] else - reverse_order.limit(1).to_a.first + relation = if order_values.empty? && primary_key + order(arel_attribute(primary_key).asc) + else + self + end + + relation = relation.offset(offset_index + index) unless index.zero? + relation.limit(limit).to_a end - end - end + end + + def find_nth_from_last(index) + if loaded? + records[-index] + else + relation = if order_values.empty? && primary_key + order(arel_attribute(primary_key).asc) + else + self + end + + relation.to_a[-index] + # TODO: can be made more performant on large result sets by + # for instance, last(index)[-index] (which would require + # refactoring the last(n) finder method to make test suite pass), + # or by using a combination of reverse_order, limit, and offset, + # e.g., reverse_order.offset(index-1).first + end + end + + def find_last(limit) + limit ? records.last(limit) : records.last + end end end diff --git a/activerecord/lib/active_record/relation/from_clause.rb b/activerecord/lib/active_record/relation/from_clause.rb index a93952fa30..8945cb0cc5 100644 --- a/activerecord/lib/active_record/relation/from_clause.rb +++ b/activerecord/lib/active_record/relation/from_clause.rb @@ -1,6 +1,6 @@ module ActiveRecord class Relation - class FromClause + class FromClause # :nodoc: attr_reader :value, :name def initialize(value, name) @@ -25,7 +25,7 @@ module ActiveRecord end def self.empty - new(nil, nil) + @empty ||= new(nil, nil) end end end diff --git a/activerecord/lib/active_record/relation/merger.rb b/activerecord/lib/active_record/relation/merger.rb index dd8f0aa298..5dac00724a 100644 --- a/activerecord/lib/active_record/relation/merger.rb +++ b/activerecord/lib/active_record/relation/merger.rb @@ -1,4 +1,4 @@ -require 'active_support/core_ext/hash/keys' +require "active_support/core_ext/hash/keys" module ActiveRecord class Relation @@ -83,78 +83,81 @@ module ActiveRecord private - def merge_preloads - return if other.preload_values.empty? && other.includes_values.empty? + def merge_preloads + return if other.preload_values.empty? && other.includes_values.empty? - if other.klass == relation.klass - relation.preload! other.preload_values unless other.preload_values.empty? - relation.includes! other.includes_values unless other.includes_values.empty? - else - reflection = relation.klass.reflect_on_all_associations.find do |r| - r.class_name == other.klass.name - end || return + if other.klass == relation.klass + relation.preload!(*other.preload_values) unless other.preload_values.empty? + relation.includes!(other.includes_values) unless other.includes_values.empty? + else + reflection = relation.klass.reflect_on_all_associations.find do |r| + r.class_name == other.klass.name + end || return - unless other.preload_values.empty? - relation.preload! reflection.name => other.preload_values - end + unless other.preload_values.empty? + relation.preload! reflection.name => other.preload_values + end - unless other.includes_values.empty? - relation.includes! reflection.name => other.includes_values + unless other.includes_values.empty? + relation.includes! reflection.name => other.includes_values + end end end - end - def merge_joins - return if other.joins_values.blank? + def merge_joins + return if other.joins_values.blank? - if other.klass == relation.klass - relation.joins!(*other.joins_values) - else - joins_dependency, rest = other.joins_values.partition do |join| - case join - when Hash, Symbol, Array - true - else - false + if other.klass == relation.klass + relation.joins!(*other.joins_values) + else + joins_dependency, rest = other.joins_values.partition do |join| + case join + when Hash, Symbol, Array + true + else + false + end end - end - join_dependency = ActiveRecord::Associations::JoinDependency.new(other.klass, - joins_dependency, - []) - relation.joins! rest + join_dependency = ActiveRecord::Associations::JoinDependency.new(other.klass, + joins_dependency, + []) + relation.joins! rest - @relation = relation.joins join_dependency + @relation = relation.joins join_dependency + end end - end - def merge_multi_values - if other.reordering_value - # override any order specified in the original relation - relation.reorder! other.order_values - elsif other.order_values - # merge in order_values from relation - relation.order! other.order_values - end + def merge_multi_values + if other.reordering_value + # override any order specified in the original relation + relation.reorder! other.order_values + elsif other.order_values + # merge in order_values from relation + relation.order! other.order_values + end - relation.extend(*other.extending_values) unless other.extending_values.blank? - end + relation.extend(*other.extending_values) unless other.extending_values.blank? + end - def merge_single_values - relation.lock_value ||= other.lock_value + def merge_single_values + if relation.from_clause.empty? + relation.from_clause = other.from_clause + end + relation.lock_value ||= other.lock_value - unless other.create_with_value.blank? - relation.create_with_value = (relation.create_with_value || {}).merge(other.create_with_value) + unless other.create_with_value.blank? + relation.create_with_value = (relation.create_with_value || {}).merge(other.create_with_value) + end end - end - def merge_clauses - CLAUSE_METHODS.each do |name| - clause = relation.send("#{name}_clause") - other_clause = other.send("#{name}_clause") - relation.send("#{name}_clause=", clause.merge(other_clause)) + def merge_clauses + CLAUSE_METHODS.each do |method| + clause = relation.get_value(method) + other_clause = other.get_value(method) + relation.set_value(method, clause.merge(other_clause)) + end end - end end end end diff --git a/activerecord/lib/active_record/relation/predicate_builder.rb b/activerecord/lib/active_record/relation/predicate_builder.rb index 43e9afe853..18ae10a652 100644 --- a/activerecord/lib/active_record/relation/predicate_builder.rb +++ b/activerecord/lib/active_record/relation/predicate_builder.rb @@ -1,12 +1,12 @@ module ActiveRecord class PredicateBuilder # :nodoc: - require 'active_record/relation/predicate_builder/array_handler' - require 'active_record/relation/predicate_builder/association_query_handler' - require 'active_record/relation/predicate_builder/base_handler' - require 'active_record/relation/predicate_builder/basic_object_handler' - require 'active_record/relation/predicate_builder/class_handler' - require 'active_record/relation/predicate_builder/range_handler' - require 'active_record/relation/predicate_builder/relation_handler' + require "active_record/relation/predicate_builder/array_handler" + require "active_record/relation/predicate_builder/association_query_handler" + require "active_record/relation/predicate_builder/base_handler" + require "active_record/relation/predicate_builder/basic_object_handler" + require "active_record/relation/predicate_builder/polymorphic_array_handler" + require "active_record/relation/predicate_builder/range_handler" + require "active_record/relation/predicate_builder/relation_handler" delegate :resolve_column_aliases, to: :table @@ -14,45 +14,33 @@ module ActiveRecord @table = table @handlers = [] - register_handler(BasicObject, BasicObjectHandler.new(self)) - register_handler(Class, ClassHandler.new(self)) + register_handler(BasicObject, BasicObjectHandler.new) register_handler(Base, BaseHandler.new(self)) - register_handler(Range, RangeHandler.new(self)) + register_handler(Range, RangeHandler.new) + register_handler(RangeHandler::RangeWithBinds, RangeHandler.new) register_handler(Relation, RelationHandler.new) register_handler(Array, ArrayHandler.new(self)) register_handler(AssociationQueryValue, AssociationQueryHandler.new(self)) + register_handler(PolymorphicArrayValue, PolymorphicArrayHandler.new(self)) end def build_from_hash(attributes) - attributes = convert_dot_notation_to_hash(attributes.stringify_keys) + attributes = convert_dot_notation_to_hash(attributes) expand_from_hash(attributes) end def create_binds(attributes) - attributes = convert_dot_notation_to_hash(attributes.stringify_keys) + attributes = convert_dot_notation_to_hash(attributes) create_binds_for_hash(attributes) end - def expand(column, value) - # Find the foreign key when using queries such as: - # Post.where(author: author) - # - # For polymorphic relationships, find the foreign key and type: - # PriceEstimate.where(estimate_of: treasure) - if table.associated_with?(column) - value = AssociationQueryValue.new(table.associated_table(column), value) - end - - build(table.arel_attribute(column), value) - end - def self.references(attributes) attributes.map do |key, value| if value.is_a?(Hash) key else key = key.to_s - key.split('.').first if key.include?('.') + key.split(".".freeze).first if key.include?(".".freeze) end end.compact end @@ -67,7 +55,7 @@ module ActiveRecord # Arel::Nodes::And.new([range.start, range.end]) # ) # end - # ActiveRecord::PredicateBuilder.register_handler(MyCustomDateRange, handler) + # ActiveRecord::PredicateBuilder.new("users").register_handler(MyCustomDateRange, handler) def register_handler(klass, handler) @handlers.unshift([klass, handler]) end @@ -76,74 +64,108 @@ module ActiveRecord handler_for(value).call(attribute, value) end + # TODO Change this to private once we've dropped Ruby 2.2 support. + # Workaround for Ruby 2.2 "private attribute?" warning. protected - attr_reader :table + attr_reader :table - def expand_from_hash(attributes) - return ["1=0"] if attributes.empty? + def expand_from_hash(attributes) + return ["1=0"] if attributes.empty? - attributes.flat_map do |key, value| - if value.is_a?(Hash) - associated_predicate_builder(key).expand_from_hash(value) - else - expand(key, value) + attributes.flat_map do |key, value| + if value.is_a?(Hash) && !table.has_column?(key) + associated_predicate_builder(key).expand_from_hash(value) + else + build(table.arel_attribute(key), value) + end end end - end + def create_binds_for_hash(attributes) + result = attributes.dup + binds = [] + + attributes.each do |column_name, value| + case + when value.is_a?(Hash) && !table.has_column?(column_name) + attrs, bvs = associated_predicate_builder(column_name).create_binds_for_hash(value) + result[column_name] = attrs + binds += bvs + next + when value.is_a?(Relation) + binds += value.bound_attributes + when value.is_a?(Range) && !table.type(column_name).respond_to?(:subtype) + first = value.begin + last = value.end + unless first.respond_to?(:infinite?) && first.infinite? + binds << build_bind_param(column_name, first) + first = Arel::Nodes::BindParam.new + end + unless last.respond_to?(:infinite?) && last.infinite? + binds << build_bind_param(column_name, last) + last = Arel::Nodes::BindParam.new + end + + result[column_name] = RangeHandler::RangeWithBinds.new(first, last, value.exclude_end?) + else + if can_be_bound?(column_name, value) + result[column_name] = Arel::Nodes::BindParam.new + binds << build_bind_param(column_name, value) + end + end - def create_binds_for_hash(attributes) - result = attributes.dup - binds = [] - - attributes.each do |column_name, value| - case value - when Hash - attrs, bvs = associated_predicate_builder(column_name).create_binds_for_hash(value) - result[column_name] = attrs - binds += bvs - when Relation - binds += value.bound_attributes - else - if can_be_bound?(column_name, value) - result[column_name] = Arel::Nodes::BindParam.new - binds << Relation::QueryAttribute.new(column_name.to_s, value, table.type(column_name)) + # Find the foreign key when using queries such as: + # Post.where(author: author) + # + # For polymorphic relationships, find the foreign key and type: + # PriceEstimate.where(estimate_of: treasure) + if table.associated_with?(column_name) + result[column_name] = AssociationQueryHandler.value_for(table, column_name, value) end end - end - [result, binds] - end + [result, binds] + end private - def associated_predicate_builder(association_name) - self.class.new(table.associated_table(association_name)) - end + def associated_predicate_builder(association_name) + self.class.new(table.associated_table(association_name)) + end - def convert_dot_notation_to_hash(attributes) - dot_notation = attributes.keys.select { |s| s.include?(".") } + def convert_dot_notation_to_hash(attributes) + dot_notation = attributes.select do |k, v| + k.include?(".".freeze) && !v.is_a?(Hash) + end - dot_notation.each do |key| - table_name, column_name = key.split(".") - value = attributes.delete(key) - attributes[table_name] ||= {} + dot_notation.each_key do |key| + table_name, column_name = key.split(".".freeze) + value = attributes.delete(key) + attributes[table_name] ||= {} - attributes[table_name] = attributes[table_name].merge(column_name => value) + attributes[table_name] = attributes[table_name].merge(column_name => value) + end + + attributes end - attributes - end + def handler_for(object) + @handlers.detect { |klass, _| klass === object }.last + end - def handler_for(object) - @handlers.detect { |klass, _| klass === object }.last - end + def can_be_bound?(column_name, value) + return if table.associated_with?(column_name) + case value + when Array, Range + table.type(column_name).respond_to?(:subtype) + else + !value.nil? && handler_for(value).is_a?(BasicObjectHandler) + end + end - def can_be_bound?(column_name, value) - !value.nil? && - handler_for(value).is_a?(BasicObjectHandler) && - !table.associated_with?(column_name) - end + def build_bind_param(column_name, value) + Relation::QueryAttribute.new(column_name.to_s, value, table.type(column_name)) + end end end diff --git a/activerecord/lib/active_record/relation/predicate_builder/array_handler.rb b/activerecord/lib/active_record/relation/predicate_builder/array_handler.rb index 95dbd6a77f..88b6c37d43 100644 --- a/activerecord/lib/active_record/relation/predicate_builder/array_handler.rb +++ b/activerecord/lib/active_record/relation/predicate_builder/array_handler.rb @@ -29,15 +29,17 @@ module ActiveRecord array_predicates.inject { |composite, predicate| composite.or(predicate) } end + # TODO Change this to private once we've dropped Ruby 2.2 support. + # Workaround for Ruby 2.2 "private attribute?" warning. protected - attr_reader :predicate_builder + attr_reader :predicate_builder - module NullPredicate # :nodoc: - def self.or(other) - other + module NullPredicate # :nodoc: + def self.or(other) + other + end end - end end end end diff --git a/activerecord/lib/active_record/relation/predicate_builder/association_query_handler.rb b/activerecord/lib/active_record/relation/predicate_builder/association_query_handler.rb index 159889d3b8..29860ec677 100644 --- a/activerecord/lib/active_record/relation/predicate_builder/association_query_handler.rb +++ b/activerecord/lib/active_record/relation/predicate_builder/association_query_handler.rb @@ -1,6 +1,17 @@ module ActiveRecord class PredicateBuilder class AssociationQueryHandler # :nodoc: + def self.value_for(table, column, value) + associated_table = table.associated_table(column) + klass = if associated_table.polymorphic_association? && ::Array === value && value.first.is_a?(Base) + PolymorphicArrayValue + else + AssociationQueryValue + end + + klass.new(associated_table, value) + end + def initialize(predicate_builder) @predicate_builder = predicate_builder end @@ -10,16 +21,18 @@ module ActiveRecord table = value.associated_table if value.base_class - queries[table.association_foreign_type] = value.base_class.name + queries[table.association_foreign_type.to_s] = value.base_class.name end - queries[table.association_foreign_key] = value.ids + queries[table.association_foreign_key.to_s] = value.ids predicate_builder.build_from_hash(queries) end + # TODO Change this to private once we've dropped Ruby 2.2 support. + # Workaround for Ruby 2.2 "private attribute?" warning. protected - attr_reader :predicate_builder + attr_reader :predicate_builder end class AssociationQueryValue # :nodoc: @@ -49,30 +62,27 @@ module ActiveRecord private - def primary_key - associated_table.association_primary_key(base_class) - end + def primary_key + associated_table.association_primary_key(base_class) + end - def polymorphic_base_class_from_value - case value - when Relation - value.klass.base_class - when Array - val = value.compact.first - val.class.base_class if val.is_a?(Base) - when Base - value.class.base_class + def polymorphic_base_class_from_value + case value + when Relation + value.klass.base_class + when Base + value.class.base_class + end end - end - def convert_to_id(value) - case value - when Base - value._read_attribute(primary_key) - else - value + def convert_to_id(value) + case value + when Base + value._read_attribute(primary_key) + else + value + end end - end end end end diff --git a/activerecord/lib/active_record/relation/predicate_builder/base_handler.rb b/activerecord/lib/active_record/relation/predicate_builder/base_handler.rb index 6fa5b16f73..3bb1037885 100644 --- a/activerecord/lib/active_record/relation/predicate_builder/base_handler.rb +++ b/activerecord/lib/active_record/relation/predicate_builder/base_handler.rb @@ -9,9 +9,11 @@ module ActiveRecord predicate_builder.build(attribute, value.id) end + # TODO Change this to private once we've dropped Ruby 2.2 support. + # Workaround for Ruby 2.2 "private attribute?" warning. protected - attr_reader :predicate_builder + attr_reader :predicate_builder end end end diff --git a/activerecord/lib/active_record/relation/predicate_builder/basic_object_handler.rb b/activerecord/lib/active_record/relation/predicate_builder/basic_object_handler.rb index 6cec75dc0a..79cde00303 100644 --- a/activerecord/lib/active_record/relation/predicate_builder/basic_object_handler.rb +++ b/activerecord/lib/active_record/relation/predicate_builder/basic_object_handler.rb @@ -1,17 +1,9 @@ module ActiveRecord class PredicateBuilder class BasicObjectHandler # :nodoc: - def initialize(predicate_builder) - @predicate_builder = predicate_builder - end - def call(attribute, value) attribute.eq(value) end - - protected - - attr_reader :predicate_builder end end end diff --git a/activerecord/lib/active_record/relation/predicate_builder/class_handler.rb b/activerecord/lib/active_record/relation/predicate_builder/class_handler.rb deleted file mode 100644 index ed313fc9d4..0000000000 --- a/activerecord/lib/active_record/relation/predicate_builder/class_handler.rb +++ /dev/null @@ -1,27 +0,0 @@ -module ActiveRecord - class PredicateBuilder - class ClassHandler # :nodoc: - def initialize(predicate_builder) - @predicate_builder = predicate_builder - end - - def call(attribute, value) - print_deprecation_warning - predicate_builder.build(attribute, value.name) - end - - protected - - attr_reader :predicate_builder - - private - - def print_deprecation_warning - ActiveSupport::Deprecation.warn(<<-MSG.squish) - Passing a class as a value in an Active Record query is deprecated and - will be removed. Pass a string instead. - MSG - end - end - end -end diff --git a/activerecord/lib/active_record/relation/predicate_builder/polymorphic_array_handler.rb b/activerecord/lib/active_record/relation/predicate_builder/polymorphic_array_handler.rb new file mode 100644 index 0000000000..335124c952 --- /dev/null +++ b/activerecord/lib/active_record/relation/predicate_builder/polymorphic_array_handler.rb @@ -0,0 +1,59 @@ +module ActiveRecord + class PredicateBuilder + class PolymorphicArrayHandler # :nodoc: + def initialize(predicate_builder) + @predicate_builder = predicate_builder + end + + def call(attribute, value) + table = value.associated_table + queries = value.type_to_ids_mapping.map do |type, ids| + { table.association_foreign_type.to_s => type, table.association_foreign_key.to_s => ids } + end + + predicates = queries.map { |query| predicate_builder.build_from_hash(query) } + + if predicates.size > 1 + type_and_ids_predicates = predicates.map { |type_predicate, id_predicate| Arel::Nodes::Grouping.new(type_predicate.and(id_predicate)) } + type_and_ids_predicates.inject(&:or) + else + predicates.first + end + end + + # TODO Change this to private once we've dropped Ruby 2.2 support. + # Workaround for Ruby 2.2 "private attribute?" warning. + protected + + attr_reader :predicate_builder + end + + class PolymorphicArrayValue # :nodoc: + attr_reader :associated_table, :values + + def initialize(associated_table, values) + @associated_table = associated_table + @values = values + end + + def type_to_ids_mapping + default_hash = Hash.new { |hsh, key| hsh[key] = [] } + values.each_with_object(default_hash) { |value, hash| hash[base_class(value).name] << convert_to_id(value) } + end + + private + + def primary_key(value) + associated_table.association_primary_key(base_class(value)) + end + + def base_class(value) + value.class.base_class + end + + def convert_to_id(value) + value._read_attribute(primary_key(value)) + end + end + end +end diff --git a/activerecord/lib/active_record/relation/predicate_builder/range_handler.rb b/activerecord/lib/active_record/relation/predicate_builder/range_handler.rb index 1b3849e3ad..5db778e19c 100644 --- a/activerecord/lib/active_record/relation/predicate_builder/range_handler.rb +++ b/activerecord/lib/active_record/relation/predicate_builder/range_handler.rb @@ -1,17 +1,25 @@ module ActiveRecord class PredicateBuilder class RangeHandler # :nodoc: - def initialize(predicate_builder) - @predicate_builder = predicate_builder - end + RangeWithBinds = Struct.new(:begin, :end, :exclude_end?) def call(attribute, value) - attribute.between(value) + if value.begin.respond_to?(:infinite?) && value.begin.infinite? + if value.end.respond_to?(:infinite?) && value.end.infinite? + attribute.not_in([]) + elsif value.exclude_end? + attribute.lt(value.end) + else + attribute.lteq(value.end) + end + elsif value.end.respond_to?(:infinite?) && value.end.infinite? + attribute.gteq(value.begin) + elsif value.exclude_end? + attribute.gteq(value.begin).and(attribute.lt(value.end)) + else + attribute.between(value) + end end - - protected - - attr_reader :predicate_builder end end end diff --git a/activerecord/lib/active_record/relation/predicate_builder/relation_handler.rb b/activerecord/lib/active_record/relation/predicate_builder/relation_handler.rb index 063150958a..8a910a82fe 100644 --- a/activerecord/lib/active_record/relation/predicate_builder/relation_handler.rb +++ b/activerecord/lib/active_record/relation/predicate_builder/relation_handler.rb @@ -3,7 +3,7 @@ module ActiveRecord class RelationHandler # :nodoc: def call(attribute, value) if value.select_values.empty? - value = value.select(value.klass.arel_table[value.klass.primary_key]) + value = value.select(value.arel_attribute(value.klass.primary_key)) end attribute.in(value.arel) diff --git a/activerecord/lib/active_record/relation/query_attribute.rb b/activerecord/lib/active_record/relation/query_attribute.rb index e69319b4de..a68e508fcc 100644 --- a/activerecord/lib/active_record/relation/query_attribute.rb +++ b/activerecord/lib/active_record/relation/query_attribute.rb @@ -1,8 +1,8 @@ -require 'active_record/attribute' +require "active_record/attribute" module ActiveRecord class Relation - class QueryAttribute < Attribute + class QueryAttribute < Attribute # :nodoc: def type_cast(value) value end diff --git a/activerecord/lib/active_record/relation/query_methods.rb b/activerecord/lib/active_record/relation/query_methods.rb index 706c99c245..78c046b07f 100644 --- a/activerecord/lib/active_record/relation/query_methods.rb +++ b/activerecord/lib/active_record/relation/query_methods.rb @@ -2,8 +2,8 @@ require "active_record/relation/from_clause" require "active_record/relation/query_attribute" require "active_record/relation/where_clause" require "active_record/relation/where_clause_factory" -require 'active_model/forbidden_attributes_protection' -require 'active_support/core_ext/string/filters' +require "active_model/forbidden_attributes_protection" +require "active_support/core_ext/string/filters" module ActiveRecord module QueryMethods @@ -14,6 +14,8 @@ module ActiveRecord # WhereChain objects act as placeholder for queries in which #where does not have any parameter. # In this case, #where must be chained with #not to return a new relation. class WhereChain + include ActiveModel::ForbiddenAttributesProtection + def initialize(scope) @scope = scope end @@ -21,7 +23,7 @@ module ActiveRecord # Returns a new relation expressing WHERE + NOT condition according to # the conditions in the arguments. # - # +not+ accepts conditions as a string, array, or hash. See #where for + # #not accepts conditions as a string, array, or hash. See QueryMethods#where for # more details on each format. # # User.where.not("name = 'Jon'") @@ -42,6 +44,8 @@ module ActiveRecord # User.where.not(name: "Jon", role: "admin") # # SELECT * FROM users WHERE name != 'Jon' AND role != 'admin' def not(opts, *rest) + opts = sanitize_forbidden_attributes(opts) + where_clause = @scope.send(:where_clause_factory).build(opts, rest) @scope.references!(PredicateBuilder.references(opts)) if Hash === opts @@ -50,55 +54,50 @@ module ActiveRecord end end - Relation::MULTI_VALUE_METHODS.each do |name| - class_eval <<-CODE, __FILE__, __LINE__ + 1 - def #{name}_values # def select_values - @values[:#{name}] || [] # @values[:select] || [] - end # end - # - def #{name}_values=(values) # def select_values=(values) - assert_mutability! # assert_mutability! - @values[:#{name}] = values # @values[:select] = values - end # end - CODE - end + FROZEN_EMPTY_ARRAY = [].freeze + FROZEN_EMPTY_HASH = {}.freeze - (Relation::SINGLE_VALUE_METHODS - [:create_with]).each do |name| + Relation::VALUE_METHODS.each do |name| + method_name = \ + case name + when *Relation::MULTI_VALUE_METHODS then "#{name}_values" + when *Relation::SINGLE_VALUE_METHODS then "#{name}_value" + when *Relation::CLAUSE_METHODS then "#{name}_clause" + end class_eval <<-CODE, __FILE__, __LINE__ + 1 - def #{name}_value # def readonly_value - @values[:#{name}] # @values[:readonly] + def #{method_name} # def includes_values + get_value(#{name.inspect}) # get_value(:includes) end # end - CODE - end - Relation::SINGLE_VALUE_METHODS.each do |name| - class_eval <<-CODE, __FILE__, __LINE__ + 1 - def #{name}_value=(value) # def readonly_value=(value) - assert_mutability! # assert_mutability! - @values[:#{name}] = value # @values[:readonly] = value + def #{method_name}=(value) # def includes_values=(value) + set_value(#{name.inspect}, value) # set_value(:includes, value) end # end CODE end - Relation::CLAUSE_METHODS.each do |name| - class_eval <<-CODE, __FILE__, __LINE__ + 1 - def #{name}_clause # def where_clause - @values[:#{name}] || new_#{name}_clause # @values[:where] || new_where_clause - end # end - # - def #{name}_clause=(value) # def where_clause=(value) - assert_mutability! # assert_mutability! - @values[:#{name}] = value # @values[:where] = value - end # end - CODE - end - def bound_attributes - from_clause.binds + arel.bind_values + where_clause.binds + having_clause.binds - end - - def create_with_value # :nodoc: - @values[:create_with] || {} + if limit_value + limit_bind = Attribute.with_cast_value( + "LIMIT".freeze, + connection.sanitize_limit(limit_value), + Type.default_value, + ) + end + if offset_value + offset_bind = Attribute.with_cast_value( + "OFFSET".freeze, + offset_value.to_i, + Type.default_value, + ) + end + connection.combine_bind_parameters( + from_clause: from_clause.binds, + join_clause: arel.bind_values, + where_clause: where_clause.binds, + having_clause: having_clause.binds, + limit: limit_bind, + offset: offset_bind, + ) end alias extensions extending_values @@ -113,7 +112,7 @@ module ActiveRecord # # allows you to access the +address+ attribute of the +User+ model without # firing an additional query. This will often result in a - # performance improvement over a simple +join+. + # performance improvement over a simple join. # # You can also specify multiple relationships, like this: # @@ -134,7 +133,7 @@ module ActiveRecord # # User.includes(:posts).where('posts.name = ?', 'example').references(:posts) # - # Note that +includes+ works with association names while +references+ needs + # Note that #includes works with association names while #references needs # the actual table name. def includes(*args) check_if_method_has_arguments!(:includes, args) @@ -152,9 +151,9 @@ module ActiveRecord # Forces eager loading by performing a LEFT OUTER JOIN on +args+: # # User.eager_load(:posts) - # => SELECT "users"."id" AS t0_r0, "users"."name" AS t0_r1, ... - # FROM "users" LEFT OUTER JOIN "posts" ON "posts"."user_id" = - # "users"."id" + # # SELECT "users"."id" AS t0_r0, "users"."name" AS t0_r1, ... + # # FROM "users" LEFT OUTER JOIN "posts" ON "posts"."user_id" = + # # "users"."id" def eager_load(*args) check_if_method_has_arguments!(:eager_load, args) spawn.eager_load!(*args) @@ -165,10 +164,10 @@ module ActiveRecord self end - # Allows preloading of +args+, in the same way that +includes+ does: + # Allows preloading of +args+, in the same way that #includes does: # # User.preload(:posts) - # => SELECT "posts".* FROM "posts" WHERE "posts"."user_id" IN (1, 2, 3) + # # SELECT "posts".* FROM "posts" WHERE "posts"."user_id" IN (1, 2, 3) def preload(*args) check_if_method_has_arguments!(:preload, args) spawn.preload!(*args) @@ -181,14 +180,14 @@ module ActiveRecord # Use to indicate that the given +table_names+ are referenced by an SQL string, # and should therefore be JOINed in any query rather than loaded separately. - # This method only works in conjunction with +includes+. + # This method only works in conjunction with #includes. # See #includes for more details. # # User.includes(:posts).where("posts.name = 'foo'") - # # => Doesn't JOIN the posts table, resulting in an error. + # # Doesn't JOIN the posts table, resulting in an error. # # User.includes(:posts).where("posts.name = 'foo'").references(:posts) - # # => Query now knows the string references posts, so adds a JOIN + # # Query now knows the string references posts, so adds a JOIN def references(*table_names) check_if_method_has_arguments!(:references, table_names) spawn.references!(*table_names) @@ -204,12 +203,12 @@ module ActiveRecord # Works in two unique ways. # - # First: takes a block so it can be used just like Array#select. + # First: takes a block so it can be used just like +Array#select+. # # Model.all.select { |m| m.field == value } # # This will build an array of objects from the database for the scope, - # converting them into an array and iterating through them using Array#select. + # converting them into an array and iterating through them using +Array#select+. # # Second: Modifies the SELECT statement for the query so that only certain # fields are retrieved: @@ -237,20 +236,27 @@ module ActiveRecord # # => "value" # # Accessing attributes of an object that do not have fields retrieved by a select - # except +id+ will throw <tt>ActiveModel::MissingAttributeError</tt>: + # except +id+ will throw ActiveModel::MissingAttributeError: # # Model.select(:field).first.other_field # # => ActiveModel::MissingAttributeError: missing attribute: other_field def select(*fields) - return super if block_given? - raise ArgumentError, 'Call this with at least one field' if fields.empty? + if block_given? + if fields.any? + raise ArgumentError, "`select' with block doesn't take arguments." + end + + return super() + end + + raise ArgumentError, "Call this with at least one field" if fields.empty? spawn._select!(*fields) end def _select!(*fields) # :nodoc: fields.flatten! fields.map! do |field| - klass.attribute_alias?(field) ? klass.attribute_alias(field) : field + klass.attribute_alias?(field) ? klass.attribute_alias(field).to_sym : field end self.select_values += fields self @@ -259,22 +265,23 @@ module ActiveRecord # Allows to specify a group attribute: # # User.group(:name) - # => SELECT "users".* FROM "users" GROUP BY name + # # SELECT "users".* FROM "users" GROUP BY name # # Returns an array with distinct records based on the +group+ attribute: # # User.select([:id, :name]) - # => [#<User id: 1, name: "Oscar">, #<User id: 2, name: "Oscar">, #<User id: 3, name: "Foo"> + # # => [#<User id: 1, name: "Oscar">, #<User id: 2, name: "Oscar">, #<User id: 3, name: "Foo">] # # User.group(:name) - # => [#<User id: 3, name: "Foo", ...>, #<User id: 2, name: "Oscar", ...>] + # # => [#<User id: 3, name: "Foo", ...>, #<User id: 2, name: "Oscar", ...>] # # User.group('name AS grouped_name, age') - # => [#<User id: 3, name: "Foo", age: 21, ...>, #<User id: 2, name: "Oscar", age: 21, ...>, #<User id: 5, name: "Foo", age: 23, ...>] + # # => [#<User id: 3, name: "Foo", age: 21, ...>, #<User id: 2, name: "Oscar", age: 21, ...>, #<User id: 5, name: "Foo", age: 23, ...>] # # Passing in an array of attributes to group by is also supported. + # # User.select([:id, :first_name]).group(:id, :first_name).first(3) - # => [#<User id: 1, first_name: "Bill">, #<User id: 2, first_name: "Earl">, #<User id: 3, first_name: "Beto">] + # # => [#<User id: 1, first_name: "Bill">, #<User id: 2, first_name: "Earl">, #<User id: 3, first_name: "Beto">] def group(*args) check_if_method_has_arguments!(:group, args) spawn.group!(*args) @@ -290,22 +297,22 @@ module ActiveRecord # Allows to specify an order attribute: # # User.order(:name) - # => SELECT "users".* FROM "users" ORDER BY "users"."name" ASC + # # SELECT "users".* FROM "users" ORDER BY "users"."name" ASC # # User.order(email: :desc) - # => SELECT "users".* FROM "users" ORDER BY "users"."email" DESC + # # SELECT "users".* FROM "users" ORDER BY "users"."email" DESC # # User.order(:name, email: :desc) - # => SELECT "users".* FROM "users" ORDER BY "users"."name" ASC, "users"."email" DESC + # # SELECT "users".* FROM "users" ORDER BY "users"."name" ASC, "users"."email" DESC # # User.order('name') - # => SELECT "users".* FROM "users" ORDER BY name + # # SELECT "users".* FROM "users" ORDER BY name # # User.order('name DESC') - # => SELECT "users".* FROM "users" ORDER BY name DESC + # # SELECT "users".* FROM "users" ORDER BY name DESC # # User.order('name DESC, email') - # => SELECT "users".* FROM "users" ORDER BY name DESC, email + # # SELECT "users".* FROM "users" ORDER BY name DESC, email def order(*args) check_if_method_has_arguments!(:order, args) spawn.order!(*args) @@ -357,15 +364,15 @@ module ActiveRecord # User.order('email DESC').select('id').where(name: "John") # .unscope(:order, :select, :where) == User.all # - # One can additionally pass a hash as an argument to unscope specific :where values. + # One can additionally pass a hash as an argument to unscope specific +:where+ values. # This is done by passing a hash with a single key-value pair. The key should be - # :where and the value should be the where value to unscope. For example: + # +:where+ and the value should be the where value to unscope. For example: # # User.where(name: "John", active: true).unscope(where: :name) # == User.where(active: true) # - # This method is similar to <tt>except</tt>, but unlike - # <tt>except</tt>, it persists across merges: + # This method is similar to #except, but unlike + # #except, it persists across merges: # # User.order('email').merge(User.except(:order)) # == User.order('email') @@ -375,7 +382,7 @@ module ActiveRecord # # This means it can be used in association definitions: # - # has_many :comments, -> { unscope where: :trashed } + # has_many :comments, -> { unscope(where: :trashed) } # def unscope(*args) check_if_method_has_arguments!(:unscope, args) @@ -389,7 +396,10 @@ module ActiveRecord args.each do |scope| case scope when Symbol - symbol_unscoping(scope) + if !VALID_UNSCOPING_VALUES.include?(scope) + raise ArgumentError, "Called unscope() with invalid unscoping argument ':#{scope}'. Valid arguments are :#{VALID_UNSCOPING_VALUES.to_a.join(", :")}." + end + set_value(scope, nil) when Hash scope.each do |key, target_value| if key != :where @@ -407,15 +417,35 @@ module ActiveRecord self end - # Performs a joins on +args+: + # Performs a joins on +args+. The given symbol(s) should match the name of + # the association(s). # # User.joins(:posts) - # => SELECT "users".* FROM "users" INNER JOIN "posts" ON "posts"."user_id" = "users"."id" + # # SELECT "users".* + # # FROM "users" + # # INNER JOIN "posts" ON "posts"."user_id" = "users"."id" + # + # Multiple joins: + # + # User.joins(:posts, :account) + # # SELECT "users".* + # # FROM "users" + # # INNER JOIN "posts" ON "posts"."user_id" = "users"."id" + # # INNER JOIN "accounts" ON "accounts"."id" = "users"."account_id" + # + # Nested joins: + # + # User.joins(posts: [:comments]) + # # SELECT "users".* + # # FROM "users" + # # INNER JOIN "posts" ON "posts"."user_id" = "users"."id" + # # INNER JOIN "comments" "comments_posts" + # # ON "comments_posts"."post_id" = "posts"."id" # # You can use strings in order to customize your joins: # # User.joins("LEFT JOIN bookmarks ON bookmarks.bookmarkable_type = 'Post' AND bookmarks.user_id = users.id") - # => SELECT "users".* FROM "users" LEFT JOIN bookmarks ON bookmarks.bookmarkable_type = 'Post' AND bookmarks.user_id = users.id + # # SELECT "users".* FROM "users" LEFT JOIN bookmarks ON bookmarks.bookmarkable_type = 'Post' AND bookmarks.user_id = users.id def joins(*args) check_if_method_has_arguments!(:joins, args) spawn.joins!(*args) @@ -428,6 +458,26 @@ module ActiveRecord self end + # Performs a left outer joins on +args+: + # + # User.left_outer_joins(:posts) + # => SELECT "users".* FROM "users" LEFT OUTER JOIN "posts" ON "posts"."user_id" = "users"."id" + # + def left_outer_joins(*args) + check_if_method_has_arguments!(:left_outer_joins, args) + + args.compact! + args.flatten! + + spawn.left_outer_joins!(*args) + end + alias :left_joins :left_outer_joins + + def left_outer_joins!(*args) # :nodoc: + self.left_outer_joins_values += args + self + end + # Returns a new relation, which is the result of filtering the current relation # according to the conditions in the arguments. # @@ -471,7 +521,7 @@ module ActiveRecord # than the previous methods; you are responsible for ensuring that the values in the template # are properly quoted. The values are passed to the connector for quoting, but the caller # is responsible for ensuring they are enclosed in quotes in the resulting SQL. After quoting, - # the values are inserted using the same escapes as the Ruby core method <tt>Kernel::sprintf</tt>. + # the values are inserted using the same escapes as the Ruby core method +Kernel::sprintf+. # # User.where(["name = '%s' and email = '%s'", "Joe", "joe@example.com"]) # # SELECT * FROM users WHERE name = 'Joe' AND email = 'joe@example.com'; @@ -548,7 +598,7 @@ module ActiveRecord # If the condition is any blank-ish object, then #where is a no-op and returns # the current relation. def where(opts = :chain, *rest) - if opts == :chain + if :chain == opts WhereChain.new(spawn) elsif opts.blank? self @@ -558,23 +608,25 @@ module ActiveRecord end def where!(opts, *rest) # :nodoc: - if Hash === opts - opts = sanitize_forbidden_attributes(opts) - references!(PredicateBuilder.references(opts)) - end - + opts = sanitize_forbidden_attributes(opts) + references!(PredicateBuilder.references(opts)) if Hash === opts self.where_clause += where_clause_factory.build(opts, rest) self end # Allows you to change a previously set where condition for a given attribute, instead of appending to that condition. # - # Post.where(trashed: true).where(trashed: false) # => WHERE `trashed` = 1 AND `trashed` = 0 - # Post.where(trashed: true).rewhere(trashed: false) # => WHERE `trashed` = 0 - # Post.where(active: true).where(trashed: true).rewhere(trashed: false) # => WHERE `active` = 1 AND `trashed` = 0 + # Post.where(trashed: true).where(trashed: false) + # # WHERE `trashed` = 1 AND `trashed` = 0 + # + # Post.where(trashed: true).rewhere(trashed: false) + # # WHERE `trashed` = 0 # - # This is short-hand for unscope(where: conditions.keys).where(conditions). Note that unlike reorder, we're only unscoping - # the named conditions -- not the entire where statement. + # Post.where(active: true).where(trashed: true).rewhere(trashed: false) + # # WHERE `active` = 1 AND `trashed` = 0 + # + # This is short-hand for <tt>unscope(where: conditions.keys).where(conditions)</tt>. + # Note that unlike reorder, we're only unscoping the named conditions -- not the entire where statement. def rewhere(conditions) unscope(where: conditions.keys).where(conditions) end @@ -583,19 +635,25 @@ module ActiveRecord # argument. # # The two relations must be structurally compatible: they must be scoping the same model, and - # they must differ only by +where+ (if no +group+ has been defined) or +having+ (if a +group+ is - # present). Neither relation may have a +limit+, +offset+, or +distinct+ set. + # they must differ only by #where (if no #group has been defined) or #having (if a #group is + # present). Neither relation may have a #limit, #offset, or #distinct set. # - # Post.where("id = 1").or(Post.where("id = 2")) - # # SELECT `posts`.* FROM `posts` WHERE (('id = 1' OR 'id = 2')) + # Post.where("id = 1").or(Post.where("author_id = 3")) + # # SELECT `posts`.* FROM `posts` WHERE ((id = 1) OR (author_id = 3)) # def or(other) + unless other.is_a? Relation + raise ArgumentError, "You have passed #{other.class.name} object to #or. Pass an ActiveRecord::Relation object instead." + end + spawn.or!(other) end def or!(other) # :nodoc: - unless structurally_compatible_for_or?(other) - raise ArgumentError, 'Relation passed to #or must be structurally compatible' + incompatible_values = structurally_incompatible_values_for_or(other) + + unless incompatible_values.empty? + raise ArgumentError, "Relation passed to #or must be structurally compatible. Incompatible values: #{incompatible_values}" end self.where_clause = self.where_clause.or(other.where_clause) @@ -604,12 +662,6 @@ module ActiveRecord self end - private def structurally_compatible_for_or?(other) # :nodoc: - Relation::SINGLE_VALUE_METHODS.all? { |m| send("#{m}_value") == other.send("#{m}_value") } && - (Relation::MULTI_VALUE_METHODS - [:extending]).all? { |m| send("#{m}_values") == other.send("#{m}_values") } && - (Relation::CLAUSE_METHODS - [:having, :where]).all? { |m| send("#{m}_clause") != other.send("#{m}_clause") } - end - # Allows to specify a HAVING clause. Note that you can't use HAVING # without also specifying a GROUP clause. # @@ -619,6 +671,7 @@ module ActiveRecord end def having!(opts, *rest) # :nodoc: + opts = sanitize_forbidden_attributes(opts) references!(PredicateBuilder.references(opts)) if Hash === opts self.having_clause += having_clause_factory.build(opts, rest) @@ -656,7 +709,7 @@ module ActiveRecord end # Specifies locking settings (default to +true+). For more information - # on locking, please see +ActiveRecord::Locking+. + # on locking, please see ActiveRecord::Locking. def lock(locks = true) spawn.lock!(locks) end @@ -687,7 +740,7 @@ module ActiveRecord # For example: # # @posts = current_user.visible_posts.where(name: params[:name]) - # # => the visible_posts method is expected to return a chainable Relation + # # the visible_posts method is expected to return a chainable Relation # # def visible_posts # case role @@ -701,7 +754,7 @@ module ActiveRecord # end # def none - where("1=0").extending!(NullRelation) + spawn.none! end def none! # :nodoc: @@ -713,7 +766,7 @@ module ActiveRecord # # users = User.readonly # users.first.save - # => ActiveRecord::ReadOnlyRecord: ActiveRecord::ReadOnlyRecord + # => ActiveRecord::ReadOnlyRecord: User is marked as readonly def readonly(value = true) spawn.readonly!(value) end @@ -732,7 +785,7 @@ module ActiveRecord # users = users.create_with(name: 'DHH') # users.new.name # => 'DHH' # - # You can pass +nil+ to +create_with+ to reset attributes: + # You can pass +nil+ to #create_with to reset attributes: # # users = users.create_with(nil) # users.new.name # => 'Oscar' @@ -754,15 +807,15 @@ module ActiveRecord # Specifies table from which the records will be fetched. For example: # # Topic.select('title').from('posts') - # # => SELECT title FROM posts + # # SELECT title FROM posts # # Can accept other relation objects. For example: # # Topic.select('title').from(Topic.approved) - # # => SELECT title FROM (SELECT * FROM topics WHERE approved = 't') subquery + # # SELECT title FROM (SELECT * FROM topics WHERE approved = 't') subquery # # Topic.select('a.title').from(Topic.approved, :a) - # # => SELECT a.title FROM (SELECT * FROM topics WHERE approved = 't') a + # # SELECT a.title FROM (SELECT * FROM topics WHERE approved = 't') a # def from(value, subquery_name = nil) spawn.from!(value, subquery_name) @@ -776,26 +829,22 @@ module ActiveRecord # Specifies whether the records should be unique or not. For example: # # User.select(:name) - # # => Might return two records with the same name + # # Might return two records with the same name # # User.select(:name).distinct - # # => Returns 1 record per distinct name + # # Returns 1 record per distinct name # # User.select(:name).distinct.distinct(false) - # # => You can also remove the uniqueness + # # You can also remove the uniqueness def distinct(value = true) spawn.distinct!(value) end - alias uniq distinct - deprecate uniq: :distinct # Like #distinct, but modifies relation in place. def distinct!(value = true) # :nodoc: self.distinct_value = value self end - alias uniq! distinct! - deprecate uniq!: :distinct! # Used to extend a scope with additional methods, either through # a module or through a block provided. @@ -870,242 +919,277 @@ module ActiveRecord @arel ||= build_arel end - private + # Returns a relation value with a given name + def get_value(name) # :nodoc: + @values[name] || default_value_for(name) + end - def assert_mutability! - raise ImmutableRelation if @loaded - raise ImmutableRelation if defined?(@arel) && @arel + # Sets the relation value with the given name + def set_value(name, value) # :nodoc: + assert_mutability! + @values[name] = value end - def build_arel - arel = Arel::SelectManager.new(table) + private - build_joins(arel, joins_values.flatten) unless joins_values.empty? + def assert_mutability! + raise ImmutableRelation if @loaded + raise ImmutableRelation if defined?(@arel) && @arel + end - arel.where(where_clause.ast) unless where_clause.empty? - arel.having(having_clause.ast) unless having_clause.empty? - arel.take(connection.sanitize_limit(limit_value)) if limit_value - arel.skip(offset_value.to_i) if offset_value - arel.group(*arel_columns(group_values.uniq.reject(&:blank?))) unless group_values.empty? + def build_arel + arel = Arel::SelectManager.new(table) - build_order(arel) + build_joins(arel, joins_values.flatten) unless joins_values.empty? + build_left_outer_joins(arel, left_outer_joins_values.flatten) unless left_outer_joins_values.empty? - build_select(arel) + arel.where(where_clause.ast) unless where_clause.empty? + arel.having(having_clause.ast) unless having_clause.empty? + arel.take(Arel::Nodes::BindParam.new) if limit_value + arel.skip(Arel::Nodes::BindParam.new) if offset_value + arel.group(*arel_columns(group_values.uniq.reject(&:blank?))) unless group_values.empty? - arel.distinct(distinct_value) - arel.from(build_from) unless from_clause.empty? - arel.lock(lock_value) if lock_value + build_order(arel) - arel - end + build_select(arel) - def symbol_unscoping(scope) - if !VALID_UNSCOPING_VALUES.include?(scope) - raise ArgumentError, "Called unscope() with invalid unscoping argument ':#{scope}'. Valid arguments are :#{VALID_UNSCOPING_VALUES.to_a.join(", :")}." - end + arel.distinct(distinct_value) + arel.from(build_from) unless from_clause.empty? + arel.lock(lock_value) if lock_value - clause_method = Relation::CLAUSE_METHODS.include?(scope) - multi_val_method = Relation::MULTI_VALUE_METHODS.include?(scope) - if clause_method - unscope_code = "#{scope}_clause=" - else - unscope_code = "#{scope}_value#{'s' if multi_val_method}=" + arel end - case scope - when :order - result = [] - else - result = [] if multi_val_method + def build_from + opts = from_clause.value + name = from_clause.name + case opts + when Relation + name ||= "subquery" + opts.arel.as(name.to_s) + else + opts + end end - self.send(unscope_code, result) - end - - def association_for_table(table_name) - table_name = table_name.to_s - @klass._reflect_on_association(table_name) || - @klass._reflect_on_association(table_name.singularize) - end + def build_left_outer_joins(manager, outer_joins) + buckets = outer_joins.group_by do |join| + case join + when Hash, Symbol, Array + :association_join + else + raise ArgumentError, "only Hash, Symbol and Array are allowed" + end + end - def build_from - opts = from_clause.value - name = from_clause.name - case opts - when Relation - name ||= 'subquery' - opts.arel.as(name.to_s) - else - opts + build_join_query(manager, buckets, Arel::Nodes::OuterJoin) end - end - def build_joins(manager, joins) - buckets = joins.group_by do |join| - case join - when String - :string_join - when Hash, Symbol, Array - :association_join - when ActiveRecord::Associations::JoinDependency - :stashed_join - when Arel::Nodes::Join - :join_node - else - raise 'unknown class: %s' % join.class.name + def build_joins(manager, joins) + buckets = joins.group_by do |join| + case join + when String + :string_join + when Hash, Symbol, Array + :association_join + when ActiveRecord::Associations::JoinDependency + :stashed_join + when Arel::Nodes::Join + :join_node + else + raise "unknown class: %s" % join.class.name + end end + + build_join_query(manager, buckets, Arel::Nodes::InnerJoin) end - buckets.default = [] - association_joins = buckets[:association_join] - stashed_association_joins = buckets[:stashed_join] - join_nodes = buckets[:join_node].uniq - string_joins = buckets[:string_join].map(&:strip).uniq + def build_join_query(manager, buckets, join_type) + buckets.default = [] - join_list = join_nodes + convert_join_strings_to_ast(manager, string_joins) + association_joins = buckets[:association_join] + stashed_association_joins = buckets[:stashed_join] + join_nodes = buckets[:join_node].uniq + string_joins = buckets[:string_join].map(&:strip).uniq - join_dependency = ActiveRecord::Associations::JoinDependency.new( - @klass, - association_joins, - join_list - ) + join_list = join_nodes + convert_join_strings_to_ast(manager, string_joins) - join_infos = join_dependency.join_constraints stashed_association_joins + join_dependency = ActiveRecord::Associations::JoinDependency.new( + @klass, + association_joins, + join_list + ) - join_infos.each do |info| - info.joins.each { |join| manager.from(join) } - manager.bind_values.concat info.binds - end + join_infos = join_dependency.join_constraints stashed_association_joins, join_type - manager.join_sources.concat(join_list) + join_infos.each do |info| + info.joins.each { |join| manager.from(join) } + manager.bind_values.concat info.binds + end - manager - end + manager.join_sources.concat(join_list) - def convert_join_strings_to_ast(table, joins) - joins - .flatten - .reject(&:blank?) - .map { |join| table.create_string_join(Arel.sql(join)) } - end + manager + end - def build_select(arel) - if select_values.any? - arel.project(*arel_columns(select_values.uniq)) - else - arel.project(@klass.arel_table[Arel.star]) + def convert_join_strings_to_ast(table, joins) + joins + .flatten + .reject(&:blank?) + .map { |join| table.create_string_join(Arel.sql(join)) } end - end - def arel_columns(columns) - columns.map do |field| - if (Symbol === field || String === field) && columns_hash.key?(field.to_s) && !from_clause.value - arel_table[field] - elsif Symbol === field - connection.quote_table_name(field.to_s) + def build_select(arel) + if select_values.any? + arel.project(*arel_columns(select_values.uniq)) else - field + arel.project(@klass.arel_table[Arel.star]) end end - end - def reverse_sql_order(order_query) - order_query = ["#{quoted_table_name}.#{quoted_primary_key} ASC"] if order_query.empty? - - order_query.flat_map do |o| - case o - when Arel::Nodes::Ordering - o.reverse - when String - o.to_s.split(',').map! do |s| - s.strip! - s.gsub!(/\sasc\Z/i, ' DESC') || s.gsub!(/\sdesc\Z/i, ' ASC') || s.concat(' DESC') + def arel_columns(columns) + columns.map do |field| + if (Symbol === field || String === field) && (klass.has_attribute?(field) || klass.attribute_alias?(field)) && !from_clause.value + arel_attribute(field) + elsif Symbol === field + connection.quote_table_name(field.to_s) + else + field end - else - o end end - end - def build_order(arel) - orders = order_values.uniq - orders.reject!(&:blank?) - - arel.order(*orders) unless orders.empty? - end - - VALID_DIRECTIONS = [:asc, :desc, :ASC, :DESC, - 'asc', 'desc', 'ASC', 'DESC'] # :nodoc: + def reverse_sql_order(order_query) + if order_query.empty? + return [arel_attribute(primary_key).desc] if primary_key + raise IrreversibleOrderError, + "Relation has no current order and table has no primary key to be used as default order" + end - def validate_order_args(args) - args.each do |arg| - next unless arg.is_a?(Hash) - arg.each do |_key, value| - raise ArgumentError, "Direction \"#{value}\" is invalid. Valid " \ - "directions are: #{VALID_DIRECTIONS.inspect}" unless VALID_DIRECTIONS.include?(value) + order_query.flat_map do |o| + case o + when Arel::Attribute + o.desc + when Arel::Nodes::Ordering + o.reverse + when String + if does_not_support_reverse?(o) + raise IrreversibleOrderError, "Order #{o.inspect} can not be reversed automatically" + end + o.split(",").map! do |s| + s.strip! + s.gsub!(/\sasc\Z/i, " DESC") || s.gsub!(/\sdesc\Z/i, " ASC") || s.concat(" DESC") + end + else + o + end end end - end - def preprocess_order_args(order_args) - order_args.flatten! - validate_order_args(order_args) + def does_not_support_reverse?(order) + # Uses SQL function with multiple arguments. + (order.include?(",") && order.split(",").find { |section| section.count("(") != section.count(")") }) || + # Uses "nulls first" like construction. + /nulls (first|last)\Z/i.match?(order) + end + + def build_order(arel) + orders = order_values.uniq + orders.reject!(&:blank?) - references = order_args.grep(String) - references.map! { |arg| arg =~ /^([a-zA-Z]\w*)\.(\w+)/ && $1 }.compact! - references!(references) if references.any? + arel.order(*orders) unless orders.empty? + end - # if a symbol is given we prepend the quoted table name - order_args.map! do |arg| - case arg - when Symbol - arg = klass.attribute_alias(arg) if klass.attribute_alias?(arg) - table[arg].asc - when Hash - arg.map { |field, dir| - field = klass.attribute_alias(field) if klass.attribute_alias?(field) - table[field].send(dir.downcase) - } - else - arg + VALID_DIRECTIONS = [:asc, :desc, :ASC, :DESC, + "asc", "desc", "ASC", "DESC"] # :nodoc: + + def validate_order_args(args) + args.each do |arg| + next unless arg.is_a?(Hash) + arg.each do |_key, value| + raise ArgumentError, "Direction \"#{value}\" is invalid. Valid " \ + "directions are: #{VALID_DIRECTIONS.inspect}" unless VALID_DIRECTIONS.include?(value) + end end - end.flatten! - end + end - # Checks to make sure that the arguments are not blank. Note that if some - # blank-like object were initially passed into the query method, then this - # method will not raise an error. - # - # Example: - # - # Post.references() # => raises an error - # Post.references([]) # => does not raise an error - # - # This particular method should be called with a method_name and the args - # passed into that method as an input. For example: - # - # def references(*args) - # check_if_method_has_arguments!("references", args) - # ... - # end - def check_if_method_has_arguments!(method_name, args) - if args.blank? - raise ArgumentError, "The method .#{method_name}() must contain arguments." + def preprocess_order_args(order_args) + order_args.map! do |arg| + klass.send(:sanitize_sql_for_order, arg) + end + order_args.flatten! + validate_order_args(order_args) + + references = order_args.grep(String) + references.map! { |arg| arg =~ /^([a-zA-Z]\w*)\.(\w+)/ && $1 }.compact! + references!(references) if references.any? + + # if a symbol is given we prepend the quoted table name + order_args.map! do |arg| + case arg + when Symbol + arel_attribute(arg).asc + when Hash + arg.map { |field, dir| + arel_attribute(field).send(dir.downcase) + } + else + arg + end + end.flatten! end - end - def new_where_clause - Relation::WhereClause.empty - end - alias new_having_clause new_where_clause + # Checks to make sure that the arguments are not blank. Note that if some + # blank-like object were initially passed into the query method, then this + # method will not raise an error. + # + # Example: + # + # Post.references() # raises an error + # Post.references([]) # does not raise an error + # + # This particular method should be called with a method_name and the args + # passed into that method as an input. For example: + # + # def references(*args) + # check_if_method_has_arguments!("references", args) + # ... + # end + def check_if_method_has_arguments!(method_name, args) + if args.blank? + raise ArgumentError, "The method .#{method_name}() must contain arguments." + end + end - def where_clause_factory - @where_clause_factory ||= Relation::WhereClauseFactory.new(klass, predicate_builder) - end - alias having_clause_factory where_clause_factory + STRUCTURAL_OR_METHODS = Relation::VALUE_METHODS - [:extending, :where, :having] + def structurally_incompatible_values_for_or(other) + STRUCTURAL_OR_METHODS.reject do |method| + get_value(method) == other.get_value(method) + end + end - def new_from_clause - Relation::FromClause.empty - end + def where_clause_factory + @where_clause_factory ||= Relation::WhereClauseFactory.new(klass, predicate_builder) + end + alias having_clause_factory where_clause_factory + + def default_value_for(name) + case name + when :create_with + FROZEN_EMPTY_HASH + when :readonly + false + when :where, :having + Relation::WhereClause.empty + when :from + Relation::FromClause.empty + when *Relation::MULTI_VALUE_METHODS + FROZEN_EMPTY_ARRAY + when *Relation::SINGLE_VALUE_METHODS + nil + else + raise ArgumentError, "unknown relation value #{name.inspect}" + end + end end end diff --git a/activerecord/lib/active_record/relation/record_fetch_warning.rb b/activerecord/lib/active_record/relation/record_fetch_warning.rb index 14e1bf89fa..31544c730e 100644 --- a/activerecord/lib/active_record/relation/record_fetch_warning.rb +++ b/activerecord/lib/active_record/relation/record_fetch_warning.rb @@ -2,15 +2,15 @@ module ActiveRecord class Relation module RecordFetchWarning # When this module is prepended to ActiveRecord::Relation and - # `config.active_record.warn_on_records_fetched_greater_than` is + # +config.active_record.warn_on_records_fetched_greater_than+ is # set to an integer, if the number of records a query returns is - # greater than the value of `warn_on_records_fetched_greater_than`, + # greater than the value of +warn_on_records_fetched_greater_than+, # a warning is logged. This allows for the detection of queries that # return a large number of records, which could cause memory bloat. # # In most cases, fetching large number of records can be performed # efficiently using the ActiveRecord::Batches methods. - # See active_record/lib/relation/batches.rb for more information. + # See ActiveRecord::Batches for more information. def exec_queries QueryRegistry.reset @@ -23,23 +23,23 @@ module ActiveRecord end end - ActiveSupport::Notifications.subscribe("sql.active_record") do |*args| - payload = args.last - + # :stopdoc: + ActiveSupport::Notifications.subscribe("sql.active_record") do |*, payload| QueryRegistry.queries << payload[:sql] end + # :startdoc: class QueryRegistry # :nodoc: extend ActiveSupport::PerThreadRegistry - attr_accessor :queries + attr_reader :queries def initialize - reset + @queries = [] end def reset - @queries = [] + @queries.clear end end end diff --git a/activerecord/lib/active_record/relation/spawn_methods.rb b/activerecord/lib/active_record/relation/spawn_methods.rb index 70da37fa84..ada89b5ec3 100644 --- a/activerecord/lib/active_record/relation/spawn_methods.rb +++ b/activerecord/lib/active_record/relation/spawn_methods.rb @@ -1,17 +1,17 @@ -require 'active_support/core_ext/hash/except' -require 'active_support/core_ext/hash/slice' -require 'active_record/relation/merger' +require "active_support/core_ext/hash/except" +require "active_support/core_ext/hash/slice" +require "active_record/relation/merger" module ActiveRecord module SpawnMethods - # This is overridden by Associations::CollectionProxy def spawn #:nodoc: clone end - # Merges in the conditions from <tt>other</tt>, if <tt>other</tt> is an <tt>ActiveRecord::Relation</tt>. + # Merges in the conditions from <tt>other</tt>, if <tt>other</tt> is an ActiveRecord::Relation. # Returns an array representing the intersection of the resulting records with <tt>other</tt>, if <tt>other</tt> is an array. + # # Post.where(published: true).joins(:comments).merge( Comment.where(spam: false) ) # # Performs a single join query with both where conditions. # @@ -28,7 +28,7 @@ module ActiveRecord # This is mainly intended for sharing common conditions between multiple associations. def merge(other) if other.is_a?(Array) - to_a & other + records & other elsif other spawn.merge!(other) else @@ -37,11 +37,14 @@ module ActiveRecord end def merge!(other) # :nodoc: - if !other.is_a?(Relation) && other.respond_to?(:to_proc) + if other.is_a?(Hash) + Relation::HashMerger.new(self, other).merge + elsif other.is_a?(Relation) + Relation::Merger.new(self, other).merge + elsif other.respond_to?(:to_proc) instance_exec(&other) else - klass = other.is_a?(Hash) ? Relation::HashMerger : Relation::Merger - klass.new(self, other).merge + raise ArgumentError, "#{other.inspect} is not an ActiveRecord::Relation" end end @@ -63,7 +66,7 @@ module ActiveRecord private - def relation_with(values) # :nodoc: + def relation_with(values) result = Relation.create(klass, table, predicate_builder, values) result.extend(*extending_values) if extending_values.any? result diff --git a/activerecord/lib/active_record/relation/where_clause.rb b/activerecord/lib/active_record/relation/where_clause.rb index 1f000b3f0f..ef0d059d1c 100644 --- a/activerecord/lib/active_record/relation/where_clause.rb +++ b/activerecord/lib/active_record/relation/where_clause.rb @@ -81,93 +81,96 @@ module ActiveRecord end def self.empty - new([], []) + @empty ||= new([], []) end + # TODO Change this to private once we've dropped Ruby 2.2 support. + # Workaround for Ruby 2.2 "private attribute?" warning. protected - attr_reader :predicates + attr_reader :predicates - def referenced_columns - @referenced_columns ||= begin - equality_nodes = predicates.select { |n| equality_node?(n) } - Set.new(equality_nodes, &:left) + def referenced_columns + @referenced_columns ||= begin + equality_nodes = predicates.select { |n| equality_node?(n) } + Set.new(equality_nodes, &:left) + end end - end private - def predicates_unreferenced_by(other) - predicates.reject do |n| - equality_node?(n) && other.referenced_columns.include?(n.left) + def predicates_unreferenced_by(other) + predicates.reject do |n| + equality_node?(n) && other.referenced_columns.include?(n.left) + end end - end - def equality_node?(node) - node.respond_to?(:operator) && node.operator == :== - end - - def non_conflicting_binds(other) - conflicts = referenced_columns & other.referenced_columns - conflicts.map! { |node| node.name.to_s } - binds.reject { |attr| conflicts.include?(attr.name) } - end + def equality_node?(node) + node.respond_to?(:operator) && node.operator == :== + end - def inverted_predicates - predicates.map { |node| invert_predicate(node) } - end + def non_conflicting_binds(other) + conflicts = referenced_columns & other.referenced_columns + conflicts.map! { |node| node.name.to_s } + binds.reject { |attr| conflicts.include?(attr.name) } + end - def invert_predicate(node) - case node - when NilClass - raise ArgumentError, 'Invalid argument for .where.not(), got nil.' - when Arel::Nodes::In - Arel::Nodes::NotIn.new(node.left, node.right) - when Arel::Nodes::Equality - Arel::Nodes::NotEqual.new(node.left, node.right) - when String - Arel::Nodes::Not.new(Arel::Nodes::SqlLiteral.new(node)) - else - Arel::Nodes::Not.new(node) + def inverted_predicates + predicates.map { |node| invert_predicate(node) } end - end - def predicates_except(columns) - predicates.reject do |node| + def invert_predicate(node) case node - when Arel::Nodes::Between, Arel::Nodes::In, Arel::Nodes::NotIn, Arel::Nodes::Equality, Arel::Nodes::NotEqual, Arel::Nodes::LessThan, Arel::Nodes::LessThanOrEqual, Arel::Nodes::GreaterThan, Arel::Nodes::GreaterThanOrEqual - subrelation = (node.left.kind_of?(Arel::Attributes::Attribute) ? node.left : node.right) - columns.include?(subrelation.name.to_s) + when NilClass + raise ArgumentError, "Invalid argument for .where.not(), got nil." + when Arel::Nodes::In + Arel::Nodes::NotIn.new(node.left, node.right) + when Arel::Nodes::Equality + Arel::Nodes::NotEqual.new(node.left, node.right) + when String + Arel::Nodes::Not.new(Arel::Nodes::SqlLiteral.new(node)) + else + Arel::Nodes::Not.new(node) end end - end - def binds_except(columns) - binds.reject do |attr| - columns.include?(attr.name) + def predicates_except(columns) + predicates.reject do |node| + case node + when Arel::Nodes::Between, Arel::Nodes::In, Arel::Nodes::NotIn, Arel::Nodes::Equality, Arel::Nodes::NotEqual, Arel::Nodes::LessThan, Arel::Nodes::LessThanOrEqual, Arel::Nodes::GreaterThan, Arel::Nodes::GreaterThanOrEqual + subrelation = (node.left.kind_of?(Arel::Attributes::Attribute) ? node.left : node.right) + columns.include?(subrelation.name.to_s) + end + end end - end - def predicates_with_wrapped_sql_literals - non_empty_predicates.map do |node| - if Arel::Nodes::Equality === node - node - else - wrap_sql_literal(node) + def binds_except(columns) + binds.reject do |attr| + columns.include?(attr.name) end end - end - def non_empty_predicates - predicates - [''] - end + def predicates_with_wrapped_sql_literals + non_empty_predicates.map do |node| + if Arel::Nodes::Equality === node + node + else + wrap_sql_literal(node) + end + end + end - def wrap_sql_literal(node) - if ::String === node - node = Arel.sql(node) + ARRAY_WITH_EMPTY_STRING = [""] + def non_empty_predicates + predicates - ARRAY_WITH_EMPTY_STRING + end + + def wrap_sql_literal(node) + if ::String === node + node = Arel.sql(node) + end + Arel::Nodes::Grouping.new(node) end - Arel::Nodes::Grouping.new(node) - end end end end diff --git a/activerecord/lib/active_record/relation/where_clause_factory.rb b/activerecord/lib/active_record/relation/where_clause_factory.rb index 0430922be3..737bc278bd 100644 --- a/activerecord/lib/active_record/relation/where_clause_factory.rb +++ b/activerecord/lib/active_record/relation/where_clause_factory.rb @@ -1,34 +1,37 @@ module ActiveRecord class Relation - class WhereClauseFactory + class WhereClauseFactory # :nodoc: def initialize(klass, predicate_builder) @klass = klass @predicate_builder = predicate_builder end def build(opts, other) - binds = [] - case opts when String, Array parts = [klass.send(:sanitize_sql, other.empty? ? opts : ([opts] + other))] when Hash attributes = predicate_builder.resolve_column_aliases(opts) attributes = klass.send(:expand_hash_conditions_for_aggregates, attributes) + attributes.stringify_keys! attributes, binds = predicate_builder.create_binds(attributes) parts = predicate_builder.build_from_hash(attributes) - else + when Arel::Nodes::Node parts = [opts] + else + raise ArgumentError, "Unsupported argument type: #{opts} (#{opts.class})" end - WhereClause.new(parts, binds) + WhereClause.new(parts, binds || []) end + # TODO Change this to private once we've dropped Ruby 2.2 support. + # Workaround for Ruby 2.2 "private attribute?" warning. protected - attr_reader :klass, :predicate_builder + attr_reader :klass, :predicate_builder end end end diff --git a/activerecord/lib/active_record/result.rb b/activerecord/lib/active_record/result.rb index 500c478e65..9ed70a9c2b 100644 --- a/activerecord/lib/active_record/result.rb +++ b/activerecord/lib/active_record/result.rb @@ -1,7 +1,8 @@ module ActiveRecord ### - # This class encapsulates a Result returned from calling +exec_query+ on any - # database connection adapter. For example: + # This class encapsulates a result returned from calling + # {#exec_query}[rdoc-ref:ConnectionAdapters::DatabaseStatements#exec_query] + # on any database connection adapter. For example: # # result = ActiveRecord::Base.connection.exec_query('SELECT id, title, body FROM posts') # result # => #<ActiveRecord::Result:0xdeadbeef> @@ -31,8 +32,6 @@ module ActiveRecord class Result include Enumerable - IDENTITY_TYPE = Type::Value.new # :nodoc: - attr_reader :columns, :rows, :column_types def initialize(columns, rows, column_types = {}) @@ -74,8 +73,14 @@ module ActiveRecord hash_rows[idx] end + def first + return nil if @rows.empty? + Hash[@columns.zip(@rows.first)] + end + def last - hash_rows.last + return nil if @rows.empty? + Hash[@columns.zip(@rows.last)] end def cast_values(type_overrides = {}) # :nodoc: @@ -96,36 +101,36 @@ module ActiveRecord private - def column_type(name, type_overrides = {}) - type_overrides.fetch(name) do - column_types.fetch(name, IDENTITY_TYPE) + def column_type(name, type_overrides = {}) + type_overrides.fetch(name) do + column_types.fetch(name, Type.default_value) + end end - end - def hash_rows - @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| - # In the past we used Hash[columns.zip(row)] - # though elegant, the verbose way is much more efficient - # both time and memory wise cause it avoids a big array allocation - # this method is called a lot and needs to be micro optimised - hash = {} - - index = 0 - length = columns.length - - while index < length - hash[columns[index]] = row[index] - index += 1 - end - - hash - } - end - end + def hash_rows + @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| + # In the past we used Hash[columns.zip(row)] + # though elegant, the verbose way is much more efficient + # both time and memory wise cause it avoids a big array allocation + # this method is called a lot and needs to be micro optimised + hash = {} + + index = 0 + length = columns.length + + while index < length + hash[columns[index]] = row[index] + index += 1 + end + + hash + } + end + end end end diff --git a/activerecord/lib/active_record/runtime_registry.rb b/activerecord/lib/active_record/runtime_registry.rb index 9d605b826a..b79eb2263f 100644 --- a/activerecord/lib/active_record/runtime_registry.rb +++ b/activerecord/lib/active_record/runtime_registry.rb @@ -1,4 +1,4 @@ -require 'active_support/per_thread_registry' +require "active_support/per_thread_registry" module ActiveRecord # This is a thread locals registry for Active Record. For example: @@ -7,14 +7,14 @@ module ActiveRecord # # returns the connection handler local to the current thread. # - # See the documentation of <tt>ActiveSupport::PerThreadRegistry</tt> + # See the documentation of ActiveSupport::PerThreadRegistry # for further details. class RuntimeRegistry # :nodoc: extend ActiveSupport::PerThreadRegistry - attr_accessor :connection_handler, :sql_runtime, :connection_id + attr_accessor :connection_handler, :sql_runtime - [:connection_handler, :sql_runtime, :connection_id].each do |val| + [:connection_handler, :sql_runtime].each do |val| class_eval %{ def self.#{val}; instance.#{val}; end }, __FILE__, __LINE__ class_eval %{ def self.#{val}=(x); instance.#{val}=x; end }, __FILE__, __LINE__ end diff --git a/activerecord/lib/active_record/sanitization.rb b/activerecord/lib/active_record/sanitization.rb index ba75ffa5a1..647834b12e 100644 --- a/activerecord/lib/active_record/sanitization.rb +++ b/activerecord/lib/active_record/sanitization.rb @@ -1,157 +1,215 @@ + module ActiveRecord module Sanitization extend ActiveSupport::Concern module ClassMethods - # Used to sanitize objects before they're used in an SQL SELECT statement. Delegates to <tt>connection.quote</tt>. - def sanitize(object) #:nodoc: - connection.quote(object) - end - alias_method :quote_value, :sanitize - - protected - - # Accepts an array 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' 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? - - case condition - when Array; sanitize_sql_array(condition) - else condition + private + + # Accepts an array or string of SQL conditions and sanitizes + # them into a valid SQL fragment for a WHERE clause. + # + # sanitize_sql_for_conditions(["name=? and group_id=?", "foo'bar", 4]) + # # => "name='foo''bar' and group_id=4" + # + # sanitize_sql_for_conditions(["name=:name and group_id=:group_id", name: "foo'bar", group_id: 4]) + # # => "name='foo''bar' and group_id='4'" + # + # sanitize_sql_for_conditions(["name='%s' and group_id='%s'", "foo'bar", 4]) + # # => "name='foo''bar' and group_id='4'" + # + # sanitize_sql_for_conditions("name='foo''bar' and group_id='4'") + # # => "name='foo''bar' and group_id='4'" + def sanitize_sql_for_conditions(condition) # :doc: + return nil if condition.blank? + + case condition + when Array; sanitize_sql_array(condition) + else condition + end end - end - alias_method :sanitize_sql, :sanitize_sql_for_conditions - alias_method :sanitize_conditions, :sanitize_sql - - # 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'" - def sanitize_sql_for_assignment(assignments, default_table_name = self.table_name) - case assignments - when Array; sanitize_sql_array(assignments) - when Hash; sanitize_sql_hash_for_assignment(assignments, default_table_name) - else assignments + alias :sanitize_sql :sanitize_sql_for_conditions + alias :sanitize_conditions :sanitize_sql + deprecate sanitize_conditions: :sanitize_sql + + # Accepts an array, hash, or string of SQL conditions and sanitizes + # them into a valid SQL fragment for a SET clause. + # + # sanitize_sql_for_assignment(["name=? and group_id=?", nil, 4]) + # # => "name=NULL and group_id=4" + # + # sanitize_sql_for_assignment(["name=:name and group_id=:group_id", name: nil, group_id: 4]) + # # => "name=NULL and group_id=4" + # + # Post.send(:sanitize_sql_for_assignment, { name: nil, group_id: 4 }) + # # => "`posts`.`name` = NULL, `posts`.`group_id` = 4" + # + # sanitize_sql_for_assignment("name=NULL and group_id='4'") + # # => "name=NULL and group_id='4'" + def sanitize_sql_for_assignment(assignments, default_table_name = self.table_name) # :doc: + case assignments + when Array; sanitize_sql_array(assignments) + when Hash; sanitize_sql_hash_for_assignment(assignments, default_table_name) + else assignments + end end - end - - # Accepts a hash of SQL conditions and replaces those attributes - # that correspond to a +composed_of+ relationship with their expanded - # aggregate attribute values. - # Given: - # class Person < ActiveRecord::Base - # 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" } - def expand_hash_conditions_for_aggregates(attrs) - expanded_attrs = {} - attrs.each do |attr, value| - if aggregation = reflect_on_aggregation(attr.to_sym) - mapping = aggregation.mapping - mapping.each do |field_attr, aggregate_attr| - if mapping.size == 1 && !value.respond_to?(aggregate_attr) - expanded_attrs[field_attr] = value - else - expanded_attrs[field_attr] = value.send(aggregate_attr) + + # Accepts an array, or string of SQL conditions and sanitizes + # them into a valid SQL fragment for an ORDER clause. + # + # sanitize_sql_for_order(["field(id, ?)", [1,3,2]]) + # # => "field(id, 1,3,2)" + # + # sanitize_sql_for_order("id ASC") + # # => "id ASC" + def sanitize_sql_for_order(condition) # :doc: + if condition.is_a?(Array) && condition.first.to_s.include?("?") + sanitize_sql_array(condition) + else + condition + end + end + + # Accepts a hash of SQL conditions and replaces those attributes + # that correspond to a {#composed_of}[rdoc-ref:Aggregations::ClassMethods#composed_of] + # relationship with their expanded aggregate attribute values. + # + # Given: + # + # class Person < ActiveRecord::Base + # 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" } + def expand_hash_conditions_for_aggregates(attrs) # :doc: + expanded_attrs = {} + attrs.each do |attr, value| + if aggregation = reflect_on_aggregation(attr.to_sym) + mapping = aggregation.mapping + mapping.each do |field_attr, aggregate_attr| + if mapping.size == 1 && !value.respond_to?(aggregate_attr) + expanded_attrs[field_attr] = value + else + expanded_attrs[field_attr] = value.send(aggregate_attr) + end end + else + expanded_attrs[attr] = value end - else - expanded_attrs[attr] = value end + expanded_attrs + end + + # Sanitizes a hash of attribute/value pairs into SQL conditions for a SET clause. + # + # sanitize_sql_hash_for_assignment({ status: nil, group_id: 1 }, "posts") + # # => "`posts`.`status` = NULL, `posts`.`group_id` = 1" + def sanitize_sql_hash_for_assignment(attrs, table) # :doc: + c = connection + attrs.map do |attr, value| + value = type_for_attribute(attr.to_s).serialize(value) + "#{c.quote_table_name_for_assignment(table, attr)} = #{c.quote(value)}" + end.join(", ") end - expanded_attrs - end - - # Sanitizes a hash of attribute/value pairs into SQL conditions for a SET clause. - # { status: nil, group_id: 1 } - # # => "status = NULL , group_id = 1" - def sanitize_sql_hash_for_assignment(attrs, table) - c = connection - attrs.map do |attr, value| - value = type_for_attribute(attr.to_s).serialize(value) - "#{c.quote_table_name_for_assignment(table, attr)} = #{c.quote(value)}" - end.join(', ') - end - - # Sanitizes a +string+ so that it is safe to use within an SQL - # LIKE statement. This method uses +escape_character+ to escape all occurrences of "\", "_" and "%" - def sanitize_sql_like(string, escape_character = "\\") - pattern = Regexp.union(escape_character, "%", "_") - string.gsub(pattern) { |x| [escape_character, x].join } - end - - # Accepts an array of conditions. The array has each value - # sanitized and interpolated into the SQL statement. - # ["name='%s' and group_id='%s'", "foo'bar", 4] returns "name='foo''bar' and group_id='4'" - def sanitize_sql_array(ary) - statement, *values = ary - if values.first.is_a?(Hash) && statement =~ /:\w+/ - replace_named_bind_variables(statement, values.first) - elsif statement.include?('?') - replace_bind_variables(statement, values) - elsif statement.blank? - statement - else - statement % values.collect { |value| connection.quote_string(value.to_s) } + + # Sanitizes a +string+ so that it is safe to use within an SQL + # LIKE statement. This method uses +escape_character+ to escape all occurrences of "\", "_" and "%". + # + # sanitize_sql_like("100%") + # # => "100\\%" + # + # sanitize_sql_like("snake_cased_string") + # # => "snake\\_cased\\_string" + # + # sanitize_sql_like("100%", "!") + # # => "100!%" + # + # sanitize_sql_like("snake_cased_string", "!") + # # => "snake!_cased!_string" + def sanitize_sql_like(string, escape_character = "\\") # :doc: + pattern = Regexp.union(escape_character, "%", "_") + string.gsub(pattern) { |x| [escape_character, x].join } end - end - - def replace_bind_variables(statement, values) #:nodoc: - raise_if_bind_arity_mismatch(statement, statement.count('?'), values.size) - bound = values.dup - c = connection - statement.gsub(/\?/) do - replace_bind_variable(bound.shift, c) + + # Accepts an array of conditions. The array has each value + # sanitized and interpolated into the SQL statement. + # + # sanitize_sql_array(["name=? and group_id=?", "foo'bar", 4]) + # # => "name='foo''bar' and group_id=4" + # + # sanitize_sql_array(["name=:name and group_id=:group_id", name: "foo'bar", group_id: 4]) + # # => "name='foo''bar' and group_id=4" + # + # sanitize_sql_array(["name='%s' and group_id='%s'", "foo'bar", 4]) + # # => "name='foo''bar' and group_id='4'" + def sanitize_sql_array(ary) # :doc: + statement, *values = ary + if values.first.is_a?(Hash) && /:\w+/.match?(statement) + replace_named_bind_variables(statement, values.first) + elsif statement.include?("?") + replace_bind_variables(statement, values) + elsif statement.blank? + statement + else + statement % values.collect { |value| connection.quote_string(value.to_s) } + end end - end - def replace_bind_variable(value, c = connection) #:nodoc: - if ActiveRecord::Relation === value - value.to_sql - else - quote_bound_value(value, c) + def replace_bind_variables(statement, values) + raise_if_bind_arity_mismatch(statement, statement.count("?"), values.size) + bound = values.dup + c = connection + statement.gsub(/\?/) do + replace_bind_variable(bound.shift, c) + end end - end - - def replace_named_bind_variables(statement, bind_vars) #:nodoc: - statement.gsub(/(:?):([a-zA-Z]\w*)/) do |match| - if $1 == ':' # skip postgresql casts - match # return the whole match - elsif bind_vars.include?(match = $2.to_sym) - replace_bind_variable(bind_vars[match]) + + def replace_bind_variable(value, c = connection) + if ActiveRecord::Relation === value + value.to_sql else - raise PreparedStatementInvalid, "missing value for :#{match} in #{statement}" + quote_bound_value(value, c) end end - 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? - c.quote(nil) + def replace_named_bind_variables(statement, bind_vars) + statement.gsub(/(:?):([a-zA-Z]\w*)/) do |match| + if $1 == ":" # skip postgresql casts + match # return the whole match + elsif bind_vars.include?(match = $2.to_sym) + replace_bind_variable(bind_vars[match]) + else + raise PreparedStatementInvalid, "missing value for :#{match} in #{statement}" + end + end + end + + def quote_bound_value(value, c = connection) + if value.respond_to?(:map) && !value.acts_like?(:string) + if value.respond_to?(:empty?) && value.empty? + c.quote(nil) + else + value.map { |v| c.quote(v) }.join(",") + end else - value.map { |v| c.quote(v) }.join(',') + c.quote(value) end - else - c.quote(value) end - end - def raise_if_bind_arity_mismatch(statement, expected, provided) #:nodoc: - unless expected == provided - raise PreparedStatementInvalid, "wrong number of bind variables (#{provided} for #{expected}) in: #{statement}" + def raise_if_bind_arity_mismatch(statement, expected, provided) + unless expected == provided + raise PreparedStatementInvalid, "wrong number of bind variables (#{provided} for #{expected}) in: #{statement}" + end end - end end # TODO: Deprecate this - def quoted_id - self.class.quote_value(@attributes[self.class.primary_key].value_for_database) + def quoted_id # :nodoc: + self.class.connection.quote(@attributes[self.class.primary_key].value_for_database) end end end diff --git a/activerecord/lib/active_record/schema.rb b/activerecord/lib/active_record/schema.rb index 0a5546a760..7a2bc9c8af 100644 --- a/activerecord/lib/active_record/schema.rb +++ b/activerecord/lib/active_record/schema.rb @@ -1,5 +1,5 @@ module ActiveRecord - # = Active Record Schema + # = Active Record \Schema # # Allows programmers to programmatically define a schema in a portable # DSL. This means you can define tables, indexes, etc. without using SQL @@ -27,29 +27,12 @@ module ActiveRecord # # ActiveRecord::Schema is only supported by database adapters that also # support migrations, the two features being very similar. - class Schema < Migration - - # Returns the migrations paths. - # - # ActiveRecord::Schema.new.migrations_paths - # # => ["db/migrate"] # Rails migration path by default. - def migrations_paths - ActiveRecord::Migrator.migrations_paths - end - - def define(info, &block) # :nodoc: - instance_eval(&block) - - unless info[:version].blank? - initialize_schema_migrations_table - connection.assume_migrated_upto_version(info[:version], migrations_paths) - end - end - + class Schema < Migration::Current # Eval the given block. All methods available to the current connection # adapter are available within the block, so you can easily use the - # database definition DSL to build up your schema (+create_table+, - # +add_index+, etc.). + # database definition DSL to build up your schema ( + # {create_table}[rdoc-ref:ConnectionAdapters::SchemaStatements#create_table], + # {add_index}[rdoc-ref:ConnectionAdapters::SchemaStatements#add_index], etc.). # # The +info+ hash is optional, and if given is used to define metadata # about the current schema (currently, only the schema's version): @@ -57,8 +40,29 @@ module ActiveRecord # ActiveRecord::Schema.define(version: 20380119000001) do # ... # end - def self.define(info={}, &block) + def self.define(info = {}, &block) new.define(info, &block) end + + def define(info, &block) # :nodoc: + instance_eval(&block) + + if info[:version].present? + initialize_schema_migrations_table + connection.assume_migrated_upto_version(info[:version], migrations_paths) + end + + ActiveRecord::InternalMetadata.create_table + ActiveRecord::InternalMetadata[:environment] = ActiveRecord::Migrator.current_environment + end + + private + # Returns the migrations paths. + # + # ActiveRecord::Schema.new.migrations_paths + # # => ["db/migrate"] # Rails migration path by default. + def migrations_paths + ActiveRecord::Migrator.migrations_paths + end end end diff --git a/activerecord/lib/active_record/schema_dumper.rb b/activerecord/lib/active_record/schema_dumper.rb index c5910fa1ad..12289511b7 100644 --- a/activerecord/lib/active_record/schema_dumper.rb +++ b/activerecord/lib/active_record/schema_dumper.rb @@ -1,4 +1,4 @@ -require 'stringio' +require "stringio" module ActiveRecord # = Active Record Schema Dumper @@ -17,7 +17,7 @@ module ActiveRecord @@ignore_tables = [] class << self - def dump(connection=ActiveRecord::Base.connection, stream=STDOUT, config = ActiveRecord::Base) + def dump(connection = ActiveRecord::Base.connection, stream = STDOUT, config = ActiveRecord::Base) new(connection, generate_options(config)).dump(stream) stream end @@ -50,10 +50,6 @@ module ActiveRecord def header(stream) define_params = @version ? "version: #{@version}" : "" - if stream.respond_to?(:external_encoding) && stream.external_encoding - stream.puts "# encoding: #{stream.external_encoding.name}" - end - stream.puts <<HEADER # This file is auto-generated from the current state of the database. Instead # of editing this file, please use the migrations feature of Active Record to @@ -89,7 +85,7 @@ HEADER end def tables(stream) - sorted_tables = @connection.tables.sort + sorted_tables = @connection.data_sources.sort - @connection.views sorted_tables.each do |table_name| table(table_name, stream) unless ignored?(table_name) @@ -104,10 +100,7 @@ HEADER end def table(table, stream) - columns = @connection.columns(table).map do |column| - column.instance_variable_set(:@table_name, table) - column - end + columns = @connection.columns(table) begin tbl = StringIO.new @@ -115,63 +108,40 @@ HEADER pk = @connection.primary_key(table) tbl.print " create_table #{remove_prefix_and_suffix(table).inspect}" - pkcol = columns.detect { |c| c.name == pk } - if pkcol - if pk != 'id' - tbl.print %Q(, primary_key: "#{pk}") - end + + case pk + when String + tbl.print ", primary_key: #{pk.inspect}" unless pk == "id" + pkcol = columns.detect { |c| c.name == pk } pkcolspec = @connection.column_spec_for_primary_key(pkcol) - if pkcolspec - pkcolspec.each do |key, value| - tbl.print ", #{key}: #{value}" - end + if pkcolspec.present? + tbl.print ", #{format_colspec(pkcolspec)}" end + when Array + tbl.print ", primary_key: #{pk.inspect}" else tbl.print ", id: false" end tbl.print ", force: :cascade" table_options = @connection.table_options(table) - tbl.print ", options: #{table_options.inspect}" unless table_options.blank? + if table_options.present? + tbl.print ", #{format_options(table_options)}" + end tbl.puts " do |t|" # then dump all non-primary key columns - column_specs = columns.map do |column| + columns.each do |column| raise StandardError, "Unknown type '#{column.sql_type}' for column '#{column.name}'" unless @connection.valid_type?(column.type) next if column.name == pk - @connection.column_spec(column) - end.compact - - # find all migration keys used in this table - keys = @connection.migration_keys - - # figure out the lengths for each column based on above keys - lengths = keys.map { |key| - column_specs.map { |spec| - spec[key] ? spec[key].length + 2 : 0 - }.max - } - - # the string we're going to sprintf our values against, with standardized column widths - format_string = lengths.map{ |len| "%-#{len}s" } - - # find the max length for the 'type' column, which is special - type_length = column_specs.map{ |column| column[:type].length }.max - - # add column type definition to our format string - format_string.unshift " t.%-#{type_length}s " - - format_string *= '' - - column_specs.each do |colspec| - values = keys.zip(lengths).map{ |key, len| colspec.key?(key) ? colspec[key] + ", " : " " * len } - values.unshift colspec[:type] - tbl.print((format_string % values).gsub(/,\s*$/, '')) + type, colspec = @connection.column_spec(column) + tbl.print " t.#{type} #{column.name.inspect}" + tbl.print ", #{format_colspec(colspec)}" if colspec.present? tbl.puts end - indexes(table, tbl) + indexes_in_create(table, tbl) tbl.puts " end" tbl.puts @@ -187,31 +157,43 @@ HEADER stream end + # Keep it for indexing materialized views def indexes(table, stream) if (indexes = @connection.indexes(table)).any? add_index_statements = indexes.map do |index| - statement_parts = [ - "t.index #{index.columns.inspect}", - "name: #{index.name.inspect}", - ] - statement_parts << 'unique: true' if index.unique - - index_lengths = (index.lengths || []).compact - statement_parts << "length: #{Hash[index.columns.zip(index.lengths)].inspect}" if index_lengths.any? - - index_orders = index.orders || {} - statement_parts << "order: #{index.orders.inspect}" if index_orders.any? - statement_parts << "where: #{index.where.inspect}" if index.where - statement_parts << "using: #{index.using.inspect}" if index.using - statement_parts << "type: #{index.type.inspect}" if index.type - - " #{statement_parts.join(', ')}" + table_name = remove_prefix_and_suffix(index.table).inspect + " add_index #{([table_name] + index_parts(index)).join(', ')}" end stream.puts add_index_statements.sort.join("\n") + stream.puts end end + def indexes_in_create(table, stream) + if (indexes = @connection.indexes(table)).any? + index_statements = indexes.map do |index| + " t.index #{index_parts(index).join(', ')}" + end + stream.puts index_statements.sort.join("\n") + end + end + + def index_parts(index) + index_parts = [ + index.columns.inspect, + "name: #{index.name.inspect}", + ] + index_parts << "unique: true" if index.unique + index_parts << "length: { #{format_options(index.lengths)} }" if index.lengths.present? + index_parts << "order: { #{format_options(index.orders)} }" if index.orders.present? + index_parts << "where: #{index.where.inspect}" if index.where + index_parts << "using: #{index.using.inspect}" if index.using + index_parts << "type: #{index.type.inspect}" if index.type + index_parts << "comment: #{index.comment.inspect}" if index.comment + index_parts + end + def foreign_keys(table, stream) if (foreign_keys = @connection.foreign_keys(table)).any? add_foreign_key_statements = foreign_keys.map do |foreign_key| @@ -242,12 +224,20 @@ HEADER end end + def format_colspec(colspec) + colspec.map { |key, value| "#{key}: #{value}" }.join(", ") + end + + def format_options(options) + options.map { |key, value| "#{key}: #{value.inspect}" }.join(", ") + end + def remove_prefix_and_suffix(table) table.gsub(/^(#{@options[:table_name_prefix]})(.+)(#{@options[:table_name_suffix]})$/, "\\2") end def ignored?(table_name) - ['schema_migrations', ignore_tables].flatten.any? do |ignored| + [ActiveRecord::Base.schema_migrations_table_name, ActiveRecord::Base.internal_metadata_table_name, ignore_tables].flatten.any? do |ignored| ignored === remove_prefix_and_suffix(table_name) end end diff --git a/activerecord/lib/active_record/schema_migration.rb b/activerecord/lib/active_record/schema_migration.rb index cb47bf23f7..5efbcff96a 100644 --- a/activerecord/lib/active_record/schema_migration.rb +++ b/activerecord/lib/active_record/schema_migration.rb @@ -1,42 +1,37 @@ -require 'active_record/scoping/default' -require 'active_record/scoping/named' +require "active_record/scoping/default" +require "active_record/scoping/named" module ActiveRecord - class SchemaMigration < ActiveRecord::Base + # This class is used to create a table that keeps track of which migrations + # have been applied to a given database. When a migration is run, its schema + # number is inserted in to the `SchemaMigration.table_name` so it doesn't need + # to be executed the next time. + class SchemaMigration < ActiveRecord::Base # :nodoc: class << self def primary_key - nil + "version" end def table_name "#{table_name_prefix}#{ActiveRecord::Base.schema_migrations_table_name}#{table_name_suffix}" end - def index_name - "#{table_name_prefix}unique_#{ActiveRecord::Base.schema_migrations_table_name}#{table_name_suffix}" - end - def table_exists? connection.table_exists?(table_name) end - def create_table(limit=nil) + def create_table unless table_exists? - version_options = {null: false} - version_options[:limit] = limit if limit + version_options = connection.internal_string_options_for_primary_key connection.create_table(table_name, id: false) do |t| - t.column :version, :string, version_options + t.string :version, version_options end - connection.add_index table_name, :version, unique: true, name: index_name end end def drop_table - if table_exists? - connection.remove_index table_name, name: index_name - connection.drop_table(table_name) - end + connection.drop_table table_name, if_exists: true end def normalize_migration_number(number) diff --git a/activerecord/lib/active_record/scoping.rb b/activerecord/lib/active_record/scoping.rb index f049b658c4..7c00e7e4ed 100644 --- a/activerecord/lib/active_record/scoping.rb +++ b/activerecord/lib/active_record/scoping.rb @@ -1,4 +1,4 @@ -require 'active_support/per_thread_registry' +require "active_support/per_thread_registry" module ActiveRecord module Scoping @@ -11,11 +11,11 @@ module ActiveRecord module ClassMethods def current_scope #:nodoc: - ScopeRegistry.value_for(:current_scope, self.to_s) + ScopeRegistry.value_for(:current_scope, self) end def current_scope=(scope) #:nodoc: - ScopeRegistry.set_value_for(:current_scope, self.to_s, scope) + ScopeRegistry.set_value_for(:current_scope, self, scope) end # Collects attributes from scopes that should be applied when creating @@ -30,15 +30,15 @@ module ActiveRecord end end - def populate_with_current_scope_attributes + def populate_with_current_scope_attributes # :nodoc: return unless self.class.scope_attributes? - self.class.scope_attributes.each do |att,value| + self.class.scope_attributes.each do |att, value| send("#{att}=", value) if respond_to?("#{att}=") end end - def initialize_internals_callback + def initialize_internals_callback # :nodoc: super populate_with_current_scope_attributes end @@ -53,18 +53,18 @@ module ActiveRecord # following code: # # registry = ActiveRecord::Scoping::ScopeRegistry - # registry.set_value_for(:current_scope, "Board", some_new_scope) + # registry.set_value_for(:current_scope, Board, some_new_scope) # # Now when you run: # - # registry.value_for(:current_scope, "Board") + # registry.value_for(:current_scope, Board) # - # You will obtain whatever was defined in +some_new_scope+. The +value_for+ - # and +set_value_for+ methods are delegated to the current +ScopeRegistry+ + # You will obtain whatever was defined in +some_new_scope+. The #value_for + # and #set_value_for methods are delegated to the current ScopeRegistry # object, so the above example code can also be called as: # # ActiveRecord::Scoping::ScopeRegistry.set_value_for(:current_scope, - # "Board", some_new_scope) + # Board, some_new_scope) class ScopeRegistry # :nodoc: extend ActiveSupport::PerThreadRegistry @@ -74,25 +74,31 @@ module ActiveRecord @registry = Hash.new { |hash, key| hash[key] = {} } end - # Obtains the value for a given +scope_name+ and +variable_name+. - def value_for(scope_type, variable_name) + # Obtains the value for a given +scope_type+ and +model+. + def value_for(scope_type, model) raise_invalid_scope_type!(scope_type) - @registry[scope_type][variable_name] + klass = model + base = model.base_class + while klass <= base + value = @registry[scope_type][klass.name] + return value if value + klass = klass.superclass + end end - # Sets the +value+ for a given +scope_type+ and +variable_name+. - def set_value_for(scope_type, variable_name, value) + # Sets the +value+ for a given +scope_type+ and +model+. + def set_value_for(scope_type, model, value) raise_invalid_scope_type!(scope_type) - @registry[scope_type][variable_name] = value + @registry[scope_type][model.name] = value end private - def raise_invalid_scope_type!(scope_type) - if !VALID_SCOPE_TYPES.include?(scope_type) - raise ArgumentError, "Invalid scope type '#{scope_type}' sent to the registry. Scope types must be included in VALID_SCOPE_TYPES" + def raise_invalid_scope_type!(scope_type) + if !VALID_SCOPE_TYPES.include?(scope_type) + raise ArgumentError, "Invalid scope type '#{scope_type}' sent to the registry. Scope types must be included in VALID_SCOPE_TYPES" + end end - end end end end diff --git a/activerecord/lib/active_record/scoping/default.rb b/activerecord/lib/active_record/scoping/default.rb index 3590b8846e..2daa48859a 100644 --- a/activerecord/lib/active_record/scoping/default.rb +++ b/activerecord/lib/active_record/scoping/default.rb @@ -6,8 +6,10 @@ module ActiveRecord included do # Stores the default scope for the class. class_attribute :default_scopes, instance_writer: false, instance_predicate: false + class_attribute :default_scope_override, instance_writer: false, instance_predicate: false self.default_scopes = [] + self.default_scope_override = nil end module ClassMethods @@ -15,7 +17,7 @@ module ActiveRecord # # class Post < ActiveRecord::Base # def self.default_scope - # where published: true + # where(published: true) # end # end # @@ -42,97 +44,105 @@ module ActiveRecord self.current_scope = nil end - protected + private - # Use this macro in your model to set a default scope for all operations on - # the model. - # - # class Article < ActiveRecord::Base - # default_scope { where(published: true) } - # end - # - # Article.all # => SELECT * FROM articles WHERE published = true - # - # 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 +call+ to the - # +default_scope+ macro, and it will be called when building the - # default scope.) - # - # 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') } - # 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 +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: - # - # class Article < ActiveRecord::Base - # def self.default_scope - # # Should return a scope, you can call 'super' here etc. - # end - # end - def default_scope(scope = nil) - scope = Proc.new if block_given? - - if scope.is_a?(Relation) || !scope.respond_to?(:call) - raise ArgumentError, - "Support for calling #default_scope without a block is removed. For example instead " \ - "of `default_scope where(color: 'red')`, please use " \ - "`default_scope { where(color: 'red') }`. (Alternatively you can just redefine " \ - "self.default_scope.)" + # Use this macro in your model to set a default scope for all operations on + # the model. + # + # class Article < ActiveRecord::Base + # default_scope { where(published: true) } + # end + # + # Article.all # => SELECT * FROM articles WHERE published = true + # + # 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 +call+ to the + # +default_scope+ macro, and it will be called when building the + # default scope.) + # + # 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') } + # 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 #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: + # + # class Article < ActiveRecord::Base + # def self.default_scope + # # Should return a scope, you can call 'super' here etc. + # end + # end + def default_scope(scope = nil) # :doc: + scope = Proc.new if block_given? + + if scope.is_a?(Relation) || !scope.respond_to?(:call) + raise ArgumentError, + "Support for calling #default_scope without a block is removed. For example instead " \ + "of `default_scope where(color: 'red')`, please use " \ + "`default_scope { where(color: 'red') }`. (Alternatively you can just redefine " \ + "self.default_scope.)" + end + + self.default_scopes += [scope] end - self.default_scopes += [scope] - end + def build_default_scope(base_rel = nil) + return if abstract_class? + + if default_scope_override.nil? + self.default_scope_override = !Base.is_a?(method(:default_scope).owner) + end - def build_default_scope(base_rel = relation) # :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 } - elsif default_scopes.any? - evaluate_default_scope do - default_scopes.inject(base_rel) do |default_scope, scope| - default_scope.merge(base_rel.scoping { scope.call }) + if default_scope_override + # The user has defined their own default scope method, so call that + evaluate_default_scope { default_scope } + elsif default_scopes.any? + base_rel ||= relation + evaluate_default_scope do + default_scopes.inject(base_rel) do |default_scope, scope| + scope = scope.respond_to?(:to_proc) ? scope : scope.method(:call) + default_scope.merge(base_rel.instance_exec(&scope)) + end end end end - end - def ignore_default_scope? # :nodoc: - ScopeRegistry.value_for(:ignore_default_scope, self) - end + def ignore_default_scope? + ScopeRegistry.value_for(:ignore_default_scope, base_class) + end - def ignore_default_scope=(ignore) # :nodoc: - ScopeRegistry.set_value_for(:ignore_default_scope, self, ignore) - end + def ignore_default_scope=(ignore) + ScopeRegistry.set_value_for(:ignore_default_scope, base_class, 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 # :nodoc: - return if ignore_default_scope? - - begin - self.ignore_default_scope = true - yield - ensure - self.ignore_default_scope = false + # 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 + return if ignore_default_scope? + + begin + self.ignore_default_scope = true + yield + ensure + self.ignore_default_scope = false + end end - end end end end diff --git a/activerecord/lib/active_record/scoping/named.rb b/activerecord/lib/active_record/scoping/named.rb index 7b62626896..27cdf8cb7e 100644 --- a/activerecord/lib/active_record/scoping/named.rb +++ b/activerecord/lib/active_record/scoping/named.rb @@ -1,6 +1,6 @@ -require 'active_support/core_ext/array' -require 'active_support/core_ext/hash/except' -require 'active_support/core_ext/kernel/singleton_class' +require "active_support/core_ext/array" +require "active_support/core_ext/hash/except" +require "active_support/core_ext/kernel/singleton_class" module ActiveRecord # = Active Record \Named \Scopes @@ -9,7 +9,7 @@ module ActiveRecord extend ActiveSupport::Concern module ClassMethods - # Returns an <tt>ActiveRecord::Relation</tt> scope object. + # Returns an ActiveRecord::Relation scope object. # # posts = Post.all # posts.size # Fires "select count(*) from posts" and returns the count @@ -20,7 +20,7 @@ module ActiveRecord # fruits = fruits.limit(10) if limited? # # You can define a scope that applies to all finders using - # <tt>ActiveRecord::Base.default_scope</tt>. + # {default_scope}[rdoc-ref:Scoping::Default::ClassMethods#default_scope]. def all if current_scope current_scope.clone @@ -39,8 +39,13 @@ module ActiveRecord end end - # Adds a class method for retrieving and querying objects. A \scope - # represents a narrowing of a database query, such as + # Adds a class method for retrieving and querying objects. + # The method is intended to return an ActiveRecord::Relation + # object, which is composable with other scopes. + # If it returns +nil+ or +false+, an + # {all}[rdoc-ref:Scoping::Named::ClassMethods#all] scope is returned instead. + # + # A \scope represents a narrowing of a database query, such as # <tt>where(color: :red).select('shirts.*').includes(:washing_instructions)</tt>. # # class Shirt < ActiveRecord::Base @@ -48,12 +53,12 @@ module ActiveRecord # scope :dry_clean_only, -> { joins(:washing_instructions).where('washing_instructions.dry_clean_only = ?', true) } # end # - # The above calls to +scope+ define class methods <tt>Shirt.red</tt> and + # The above calls to #scope define class methods <tt>Shirt.red</tt> and # <tt>Shirt.dry_clean_only</tt>. <tt>Shirt.red</tt>, in effect, # represents the query <tt>Shirt.where(color: 'red')</tt>. # # You should always pass a callable object to the scopes defined - # with +scope+. This ensures that the scope is re-evaluated each + # with #scope. This ensures that the scope is re-evaluated each # time it is called. # # Note that this is simply 'syntactic sugar' for defining an actual @@ -66,14 +71,15 @@ module ActiveRecord # end # # Unlike <tt>Shirt.find(...)</tt>, however, the object returned by - # <tt>Shirt.red</tt> is not an Array; it resembles the association object - # constructed by a +has_many+ declaration. For instance, you can invoke - # <tt>Shirt.red.first</tt>, <tt>Shirt.red.count</tt>, + # <tt>Shirt.red</tt> is not an Array but an ActiveRecord::Relation, + # which is composable with other scopes; it resembles the association object + # constructed by a {has_many}[rdoc-ref:Associations::ClassMethods#has_many] + # declaration. For instance, you can invoke <tt>Shirt.red.first</tt>, <tt>Shirt.red.count</tt>, # <tt>Shirt.red.where(size: 'small')</tt>. Also, just as with the # association objects, named \scopes act like an Array, implementing # Enumerable; <tt>Shirt.red.each(&block)</tt>, <tt>Shirt.red.first</tt>, # and <tt>Shirt.red.inject(memo, &block)</tt> all behave as if - # <tt>Shirt.red</tt> really was an Array. + # <tt>Shirt.red</tt> really was an array. # # These named \scopes are composable. For instance, # <tt>Shirt.red.dry_clean_only</tt> will produce all shirts that are @@ -84,7 +90,8 @@ module ActiveRecord # # All scopes are available as class methods on the ActiveRecord::Base # descendant upon which the \scopes were defined. But they are also - # available to +has_many+ associations. If, + # available to {has_many}[rdoc-ref:Associations::ClassMethods#has_many] + # associations. If, # # class Person < ActiveRecord::Base # has_many :shirts @@ -93,8 +100,8 @@ module ActiveRecord # then <tt>elton.shirts.red.dry_clean_only</tt> will return all of # Elton's red, dry clean only shirts. # - # \Named scopes can also have extensions, just as with +has_many+ - # declarations: + # \Named scopes can also have extensions, just as with + # {has_many}[rdoc-ref:Associations::ClassMethods#has_many] declarations: # # class Shirt < ActiveRecord::Base # scope :red, -> { where(color: 'red') } do @@ -135,7 +142,7 @@ module ActiveRecord # Article.featured.titles def scope(name, body, &block) unless body.respond_to?(:call) - raise ArgumentError, 'The scope body needs to be callable.' + raise ArgumentError, "The scope body needs to be callable." end if dangerous_class_method?(name) @@ -144,6 +151,7 @@ module ActiveRecord "a class method with the same name." end + valid_scope_name?(name) extension = Module.new(&block) if block if body.respond_to?(:to_proc) @@ -162,6 +170,15 @@ module ActiveRecord end end end + + private + + def valid_scope_name?(name) + if respond_to?(name, true) && logger + logger.warn "Creating scope :#{name}. " \ + "Overwriting existing method #{self.name}.#{name}." + end + end end end end diff --git a/activerecord/lib/active_record/secure_token.rb b/activerecord/lib/active_record/secure_token.rb index ca11853da7..7606961e2e 100644 --- a/activerecord/lib/active_record/secure_token.rb +++ b/activerecord/lib/active_record/secure_token.rb @@ -3,7 +3,7 @@ module ActiveRecord extend ActiveSupport::Concern module ClassMethods - # Example using has_secure_token + # Example using #has_secure_token # # # Schema: User(token:string, auth_token:string) # class User < ActiveRecord::Base @@ -18,16 +18,16 @@ module ActiveRecord # user.regenerate_token # => true # user.regenerate_auth_token # => true # - # SecureRandom::base58 is used to generate the 24-character unique token, so collisions are highly unlikely. + # <tt>SecureRandom::base58</tt> is used to generate the 24-character unique token, so collisions are highly unlikely. # # Note that it's still possible to generate a race condition in the database in the same way that - # <tt>validates_uniqueness_of</tt> can. You're encouraged to add a unique index in the database to deal - # with this even more unlikely scenario. + # {validates_uniqueness_of}[rdoc-ref:Validations::ClassMethods#validates_uniqueness_of] can. + # You're encouraged to add a unique index in the database to deal with this even more unlikely scenario. def has_secure_token(attribute = :token) # Load securerandom only when has_secure_token is used. - require 'active_support/core_ext/securerandom' + require "active_support/core_ext/securerandom" define_method("regenerate_#{attribute}") { update! attribute => self.class.generate_unique_secure_token } - before_create { self.send("#{attribute}=", self.class.generate_unique_secure_token) unless self.send("#{attribute}?")} + before_create { self.send("#{attribute}=", self.class.generate_unique_secure_token) unless self.send("#{attribute}?") } end def generate_unique_secure_token diff --git a/activerecord/lib/active_record/serialization.rb b/activerecord/lib/active_record/serialization.rb index 48c12dcf9f..5a408e7b8e 100644 --- a/activerecord/lib/active_record/serialization.rb +++ b/activerecord/lib/active_record/serialization.rb @@ -1,5 +1,5 @@ module ActiveRecord #:nodoc: - # = Active Record Serialization + # = Active Record \Serialization module Serialization extend ActiveSupport::Concern include ActiveModel::Serializers::JSON @@ -18,5 +18,3 @@ module ActiveRecord #:nodoc: end end end - -require 'active_record/serializers/xml_serializer' diff --git a/activerecord/lib/active_record/serializers/xml_serializer.rb b/activerecord/lib/active_record/serializers/xml_serializer.rb deleted file mode 100644 index 89b7e0be82..0000000000 --- a/activerecord/lib/active_record/serializers/xml_serializer.rb +++ /dev/null @@ -1,193 +0,0 @@ -require 'active_support/core_ext/hash/conversions' - -module ActiveRecord #:nodoc: - module Serialization - include ActiveModel::Serializers::Xml - - # Builds an XML document to represent the model. Some configuration is - # available through +options+. However more complicated cases should - # override ActiveRecord::Base#to_xml. - # - # By default the generated XML document will include the processing - # instruction and all the object's attributes. For example: - # - # <?xml version="1.0" encoding="UTF-8"?> - # <topic> - # <title>The First Topic</title> - # <author-name>David</author-name> - # <id type="integer">1</id> - # <approved type="boolean">false</approved> - # <replies-count type="integer">0</replies-count> - # <bonus-time type="dateTime">2000-01-01T08:28:00+12:00</bonus-time> - # <written-on type="dateTime">2003-07-16T09:28:00+1200</written-on> - # <content>Have a nice day</content> - # <author-email-address>david@loudthinking.com</author-email-address> - # <parent-id></parent-id> - # <last-read type="date">2004-04-15</last-read> - # </topic> - # - # This behavior can be controlled with <tt>:only</tt>, <tt>:except</tt>, - # <tt>:skip_instruct</tt>, <tt>:skip_types</tt>, <tt>:dasherize</tt> and <tt>:camelize</tt> . - # The <tt>:only</tt> and <tt>:except</tt> options are the same as for the - # +attributes+ method. The default is to dasherize all column names, but you - # can disable this setting <tt>:dasherize</tt> to +false+. Setting <tt>:camelize</tt> - # to +true+ will camelize all column names - this also overrides <tt>:dasherize</tt>. - # To not have the column type included in the XML output set <tt>:skip_types</tt> to +true+. - # - # For instance: - # - # topic.to_xml(skip_instruct: true, except: [ :id, :bonus_time, :written_on, :replies_count ]) - # - # <topic> - # <title>The First Topic</title> - # <author-name>David</author-name> - # <approved type="boolean">false</approved> - # <content>Have a nice day</content> - # <author-email-address>david@loudthinking.com</author-email-address> - # <parent-id></parent-id> - # <last-read type="date">2004-04-15</last-read> - # </topic> - # - # To include first level associations use <tt>:include</tt>: - # - # firm.to_xml include: [ :account, :clients ] - # - # <?xml version="1.0" encoding="UTF-8"?> - # <firm> - # <id type="integer">1</id> - # <rating type="integer">1</rating> - # <name>37signals</name> - # <clients type="array"> - # <client> - # <rating type="integer">1</rating> - # <name>Summit</name> - # </client> - # <client> - # <rating type="integer">1</rating> - # <name>Microsoft</name> - # </client> - # </clients> - # <account> - # <id type="integer">1</id> - # <credit-limit type="integer">50</credit-limit> - # </account> - # </firm> - # - # Additionally, the record being serialized will be passed to a Proc's second - # parameter. This allows for ad hoc additions to the resultant document that - # incorporate the context of the record being serialized. And by leveraging the - # closure created by a Proc, to_xml can be used to add elements that normally fall - # outside of the scope of the model -- for example, generating and appending URLs - # associated with models. - # - # proc = Proc.new { |options, record| options[:builder].tag!('name-reverse', record.name.reverse) } - # firm.to_xml procs: [ proc ] - # - # <firm> - # # ... normal attributes as shown above ... - # <name-reverse>slangis73</name-reverse> - # </firm> - # - # To include deeper levels of associations pass a hash like this: - # - # firm.to_xml include: {account: {}, clients: {include: :address}} - # <?xml version="1.0" encoding="UTF-8"?> - # <firm> - # <id type="integer">1</id> - # <rating type="integer">1</rating> - # <name>37signals</name> - # <clients type="array"> - # <client> - # <rating type="integer">1</rating> - # <name>Summit</name> - # <address> - # ... - # </address> - # </client> - # <client> - # <rating type="integer">1</rating> - # <name>Microsoft</name> - # <address> - # ... - # </address> - # </client> - # </clients> - # <account> - # <id type="integer">1</id> - # <credit-limit type="integer">50</credit-limit> - # </account> - # </firm> - # - # To include any methods on the model being called use <tt>:methods</tt>: - # - # firm.to_xml methods: [ :calculated_earnings, :real_earnings ] - # - # <firm> - # # ... normal attributes as shown above ... - # <calculated-earnings>100000000000000000</calculated-earnings> - # <real-earnings>5</real-earnings> - # </firm> - # - # To call any additional Procs use <tt>:procs</tt>. The Procs are passed a - # 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> - # # ... normal attributes as shown above ... - # <abc>def</abc> - # </firm> - # - # Alternatively, you can yield the builder object as part of the +to_xml+ call: - # - # firm.to_xml do |xml| - # xml.creator do - # xml.first_name "David" - # xml.last_name "Heinemeier Hansson" - # end - # end - # - # <firm> - # # ... normal attributes as shown above ... - # <creator> - # <first_name>David</first_name> - # <last_name>Heinemeier Hansson</last_name> - # </creator> - # </firm> - # - # As noted above, you may override +to_xml+ in your ActiveRecord::Base - # subclasses to have complete control about what's generated. The general - # form of doing this is: - # - # class IHaveMyOwnXML < ActiveRecord::Base - # def to_xml(options = {}) - # require 'builder' - # options[:indent] ||= 2 - # xml = options[:builder] ||= ::Builder::XmlMarkup.new(indent: options[:indent]) - # xml.instruct! unless options[:skip_instruct] - # xml.level_one do - # xml.tag!(:second_level, 'content') - # end - # end - # end - def to_xml(options = {}, &block) - XmlSerializer.new(self, options).serialize(&block) - end - end - - class XmlSerializer < ActiveModel::Serializers::Xml::Serializer #:nodoc: - class Attribute < ActiveModel::Serializers::Xml::Serializer::Attribute #:nodoc: - def compute_type - klass = @serializable.class - cast_type = klass.type_for_attribute(name) - - type = ActiveSupport::XmlMini::TYPE_NAMES[value.class.name] || cast_type.type - - { :text => :string, - :time => :datetime }[type] || type - end - protected :compute_type - end - end -end diff --git a/activerecord/lib/active_record/statement_cache.rb b/activerecord/lib/active_record/statement_cache.rb index 95986c820c..1877489e55 100644 --- a/activerecord/lib/active_record/statement_cache.rb +++ b/activerecord/lib/active_record/statement_cache.rb @@ -1,5 +1,4 @@ module ActiveRecord - # Statement cache is used to cache a single statement in order to avoid creating the AST again. # Initializing the cache is done by passing the statement in the create block: # @@ -7,12 +6,14 @@ module ActiveRecord # Book.where(name: "my book").where("author_id > 3") # end # - # The cached statement is executed by using the +execute+ method: + # The cached statement is executed by using the + # {connection.execute}[rdoc-ref:ConnectionAdapters::DatabaseStatements#execute] method: # # cache.execute([], Book, Book.connection) # - # The relation returned by the block is cached, and for each +execute+ call the cached relation gets duped. - # Database is queried when +to_a+ is called on the relation. + # The relation returned by the block is cached, and for each + # {execute}[rdoc-ref:ConnectionAdapters::DatabaseStatements#execute] + # call the cached relation gets duped. Database is queried when +to_a+ is called on the relation. # # If you want to cache the statement without the values you can use the +bind+ method of the # block parameter. @@ -38,28 +39,27 @@ module ActiveRecord end class PartialQuery < Query # :nodoc: - def initialize values + def initialize(values) @values = values - @indexes = values.each_with_index.find_all { |thing,i| + @indexes = values.each_with_index.find_all { |thing, i| Arel::Nodes::BindParam === thing }.map(&:last) end def sql_for(binds, connection) val = @values.dup - binds = connection.prepare_binds_for_database(binds) - @indexes.each { |i| val[i] = connection.quote(binds.shift) } + casted_binds = binds.map(&:value_for_database) + @indexes.each { |i| val[i] = connection.quote(casted_binds.shift) } val.join end end - def self.query(visitor, ast) - Query.new visitor.accept(ast, Arel::Collectors::SQLString.new).value + def self.query(sql) + Query.new(sql) end - def self.partial_query(visitor, ast, collector) - collected = visitor.accept(ast, collector).value - PartialQuery.new collected + def self.partial_query(values) + PartialQuery.new(values) end class Params # :nodoc: @@ -68,7 +68,7 @@ module ActiveRecord class BindMap # :nodoc: def initialize(bound_attributes) - @indexes = [] + @indexes = [] @bound_attributes = bound_attributes bound_attributes.each_with_index do |attr, i| @@ -80,7 +80,7 @@ module ActiveRecord def bind(values) bas = @bound_attributes.dup - @indexes.each_with_index { |offset,i| bas[offset] = bas[offset].with_cast_value(values[i]) } + @indexes.each_with_index { |offset, i| bas[offset] = bas[offset].with_cast_value(values[i]) } bas end end @@ -90,7 +90,7 @@ module ActiveRecord def self.create(connection, block = Proc.new) relation = block.call Params.new bind_map = BindMap.new relation.bound_attributes - query_builder = connection.cacheable_query relation.arel + query_builder = connection.cacheable_query(self, relation.arel) new query_builder, bind_map end @@ -99,12 +99,12 @@ module ActiveRecord @bind_map = bind_map end - def execute(params, klass, connection) + def execute(params, klass, connection, &block) bind_values = bind_map.bind params sql = query_builder.sql_for bind_values, connection - klass.find_by_sql sql, bind_values + klass.find_by_sql(sql, bind_values, preparable: true, &block) end alias :call :execute end diff --git a/activerecord/lib/active_record/store.rb b/activerecord/lib/active_record/store.rb index 919bc58ba5..d4be20d999 100644 --- a/activerecord/lib/active_record/store.rb +++ b/activerecord/lib/active_record/store.rb @@ -1,4 +1,4 @@ -require 'active_support/core_ext/hash/indifferent_access' +require "active_support/core_ext/hash/indifferent_access" module ActiveRecord # Store gives you a thin wrapper around serialize for the purpose of storing hashes in a single column. @@ -16,7 +16,8 @@ module ActiveRecord # JSON, YAML, Marshal are supported out of the box. Generally it can be any wrapper that provides +load+ and +dump+. # # NOTE: If you are using PostgreSQL specific columns like +hstore+ or +json+ there is no need for - # the serialization provided by +store+. Simply use +store_accessor+ instead to generate + # the serialization provided by {.store}[rdoc-ref:rdoc-ref:ClassMethods#store]. + # Simply use {.store_accessor}[rdoc-ref:ClassMethods#store_accessor] instead to generate # the accessor methods. Be aware that these columns use a string keyed hash and do not allow access # using a symbol. # @@ -43,7 +44,7 @@ module ActiveRecord # store_accessor :settings, :privileges, :servants # end # - # The stored attribute names can be retrieved using +stored_attributes+. + # The stored attribute names can be retrieved using {.stored_attributes}[rdoc-ref:rdoc-ref:ClassMethods#stored_attributes]. # # User.stored_attributes[:settings] # [:color, :homepage] # @@ -113,25 +114,24 @@ module ActiveRecord def stored_attributes parent = superclass.respond_to?(:stored_attributes) ? superclass.stored_attributes : {} - if self.local_stored_attributes - parent.merge!(self.local_stored_attributes) { |k, a, b| a | b } + if local_stored_attributes + parent.merge!(local_stored_attributes) { |k, a, b| a | b } end parent end end - protected - def read_store_attribute(store_attribute, key) + private + def read_store_attribute(store_attribute, key) # :doc: accessor = store_accessor_for(store_attribute) accessor.read(self, store_attribute, key) end - def write_store_attribute(store_attribute, key, value) + def write_store_attribute(store_attribute, key, value) # :doc: accessor = store_accessor_for(store_attribute) accessor.write(self, store_attribute, key, value) end - private def store_accessor_for(store_attribute) type_for_attribute(store_attribute.to_s).accessor end @@ -176,34 +176,34 @@ module ActiveRecord end end - class IndifferentCoder # :nodoc: - def initialize(coder_or_class_name) - @coder = - if coder_or_class_name.respond_to?(:load) && coder_or_class_name.respond_to?(:dump) - coder_or_class_name - else - ActiveRecord::Coders::YAMLColumn.new(coder_or_class_name || Object) - end - end + class IndifferentCoder # :nodoc: + def initialize(coder_or_class_name) + @coder = + if coder_or_class_name.respond_to?(:load) && coder_or_class_name.respond_to?(:dump) + coder_or_class_name + else + ActiveRecord::Coders::YAMLColumn.new(coder_or_class_name || Object) + end + end - def dump(obj) - @coder.dump self.class.as_indifferent_hash(obj) - end + def dump(obj) + @coder.dump self.class.as_indifferent_hash(obj) + end - def load(yaml) - self.class.as_indifferent_hash(@coder.load(yaml || '')) - end + def load(yaml) + self.class.as_indifferent_hash(@coder.load(yaml || "")) + end - def self.as_indifferent_hash(obj) - case obj - when ActiveSupport::HashWithIndifferentAccess - obj - when Hash - obj.with_indifferent_access - else - ActiveSupport::HashWithIndifferentAccess.new + def self.as_indifferent_hash(obj) + case obj + when ActiveSupport::HashWithIndifferentAccess + obj + when Hash + obj.with_indifferent_access + else + ActiveSupport::HashWithIndifferentAccess.new + end end end - end end end diff --git a/activerecord/lib/active_record/suppressor.rb b/activerecord/lib/active_record/suppressor.rb index b3644bf569..d9acb1a1dc 100644 --- a/activerecord/lib/active_record/suppressor.rb +++ b/activerecord/lib/active_record/suppressor.rb @@ -30,14 +30,19 @@ module ActiveRecord module ClassMethods def suppress(&block) + previous_state = SuppressorRegistry.suppressed[name] SuppressorRegistry.suppressed[name] = true yield ensure - SuppressorRegistry.suppressed[name] = false + SuppressorRegistry.suppressed[name] = previous_state end end - def create_or_update(*args) # :nodoc: + def save(*) # :nodoc: + SuppressorRegistry.suppressed[self.class.name] ? true : super + end + + def save!(*) # :nodoc: SuppressorRegistry.suppressed[self.class.name] ? true : super end end diff --git a/activerecord/lib/active_record/table_metadata.rb b/activerecord/lib/active_record/table_metadata.rb index 41f1c55c3c..b618e5cfcd 100644 --- a/activerecord/lib/active_record/table_metadata.rb +++ b/activerecord/lib/active_record/table_metadata.rb @@ -10,36 +10,45 @@ module ActiveRecord end def resolve_column_aliases(hash) - hash = hash.dup - hash.keys.grep(Symbol) do |key| - if klass.attribute_alias? key - hash[klass.attribute_alias(key)] = hash.delete key + new_hash = hash.dup + hash.each do |key, _| + if (key.is_a?(Symbol)) && klass.attribute_alias?(key) + new_hash[klass.attribute_alias(key)] = new_hash.delete(key) end end - hash + new_hash end def arel_attribute(column_name) - arel_table[column_name] + if klass + klass.arel_attribute(column_name, arel_table) + else + arel_table[column_name] + end end def type(column_name) if klass klass.type_for_attribute(column_name.to_s) else - Type::Value.new + Type.default_value end end + def has_column?(column_name) + klass && klass.columns_hash.key?(column_name.to_s) + end + def associated_with?(association_name) klass && klass._reflect_on_association(association_name) end def associated_table(table_name) - return self if table_name == arel_table.name + association = klass._reflect_on_association(table_name) || klass._reflect_on_association(table_name.singularize) - association = klass._reflect_on_association(table_name) - if association && !association.polymorphic? + if !association && table_name == arel_table.name + return self + elsif association && !association.polymorphic? association_klass = association.klass arel_table = association_klass.arel_table.alias(table_name) else @@ -55,8 +64,10 @@ module ActiveRecord association && association.polymorphic? end + # TODO Change this to private once we've dropped Ruby 2.2 support. + # Workaround for Ruby 2.2 "private attribute?" warning. protected - attr_reader :klass, :arel_table, :association + attr_reader :klass, :arel_table, :association end end diff --git a/activerecord/lib/active_record/tasks/database_tasks.rb b/activerecord/lib/active_record/tasks/database_tasks.rb index 683741768b..6f868b6b7e 100644 --- a/activerecord/lib/active_record/tasks/database_tasks.rb +++ b/activerecord/lib/active_record/tasks/database_tasks.rb @@ -1,11 +1,11 @@ -require 'active_support/core_ext/string/filters' +require "active_support/core_ext/string/filters" module ActiveRecord module Tasks # :nodoc: class DatabaseAlreadyExists < StandardError; end # :nodoc: class DatabaseNotSupported < StandardError; end # :nodoc: - # <tt>ActiveRecord::Tasks::DatabaseTasks</tt> is a utility class, which encapsulates + # ActiveRecord::Tasks::DatabaseTasks is a utility class, which encapsulates # logic behind common tasks used to manage database and migrations. # # The tasks defined here are used with Rake tasks provided by Active Record. @@ -18,15 +18,15 @@ module ActiveRecord # # The possible config values are: # - # * +env+: current environment (like Rails.env). - # * +database_configuration+: configuration of your databases (as in +config/database.yml+). - # * +db_dir+: your +db+ directory. - # * +fixtures_path+: a path to fixtures directory. - # * +migrations_paths+: a list of paths to directories with migrations. - # * +seed_loader+: an object which will load seeds, it needs to respond to the +load_seed+ method. - # * +root+: a path to the root of the application. + # * +env+: current environment (like Rails.env). + # * +database_configuration+: configuration of your databases (as in +config/database.yml+). + # * +db_dir+: your +db+ directory. + # * +fixtures_path+: a path to fixtures directory. + # * +migrations_paths+: a list of paths to directories with migrations. + # * +seed_loader+: an object which will load seeds, it needs to respond to the +load_seed+ method. + # * +root+: a path to the root of the application. # - # Example usage of +DatabaseTasks+ outside Rails could look as such: + # Example usage of DatabaseTasks outside Rails could look as such: # # include ActiveRecord::Tasks # DatabaseTasks.database_configuration = YAML.load_file('my_database_config.yml') @@ -35,12 +35,38 @@ module ActiveRecord # # DatabaseTasks.create_current('production') module DatabaseTasks + ## + # :singleton-method: + # Extra flags passed to database CLI tool (mysqldump/pg_dump) when calling db:structure:dump + mattr_accessor :structure_dump_flags, instance_accessor: false + + ## + # :singleton-method: + # Extra flags passed to database CLI tool when calling db:structure:load + mattr_accessor :structure_load_flags, instance_accessor: false + extend self attr_writer :current_config, :db_dir, :migrations_paths, :fixtures_path, :root, :env, :seed_loader attr_accessor :database_configuration - LOCAL_HOSTS = ['127.0.0.1', 'localhost'] + LOCAL_HOSTS = ["127.0.0.1", "localhost"] + + def check_protected_environments! + unless ENV["DISABLE_DATABASE_ENVIRONMENT_CHECK"] + current = ActiveRecord::Migrator.current_environment + stored = ActiveRecord::Migrator.last_stored_environment + + if ActiveRecord::Migrator.protected_environment? + raise ActiveRecord::ProtectedEnvironmentError.new(stored) + end + + if stored && stored != current + raise ActiveRecord::EnvironmentMismatchError.new(current: current, stored: stored) + end + end + rescue ActiveRecord::NoDatabaseError + end def register_task(pattern, task) @tasks ||= {} @@ -56,15 +82,15 @@ module ActiveRecord end def migrations_paths - @migrations_paths ||= Rails.application.paths['db/migrate'].to_a + @migrations_paths ||= Rails.application.paths["db/migrate"].to_a end def fixtures_path - @fixtures_path ||= if ENV['FIXTURES_PATH'] - File.join(root, ENV['FIXTURES_PATH']) - else - File.join(root, 'test', 'fixtures') - end + @fixtures_path ||= if ENV["FIXTURES_PATH"] + File.join(root, ENV["FIXTURES_PATH"]) + else + File.join(root, "test", "fixtures") + end end def root @@ -80,7 +106,7 @@ module ActiveRecord end def current_config(options = {}) - options.reverse_merge! :env => env + options.reverse_merge! env: env if options.has_key?(:config) @current_config = options[:config] else @@ -90,16 +116,22 @@ module ActiveRecord def create(*arguments) configuration = arguments.first - class_for_adapter(configuration['adapter']).new(*arguments).create + class_for_adapter(configuration["adapter"]).new(*arguments).create + $stdout.puts "Created database '#{configuration['database']}'" rescue DatabaseAlreadyExists - $stderr.puts "#{configuration['database']} already exists" + $stderr.puts "Database '#{configuration['database']}' already exists" rescue Exception => error - $stderr.puts error, *(error.backtrace) + $stderr.puts error $stderr.puts "Couldn't create database for #{configuration.inspect}" + raise end def create_all + old_pool = ActiveRecord::Base.connection_handler.retrieve_connection_pool(ActiveRecord::Base.connection_specification_name) each_local_configuration { |configuration| create configuration } + if old_pool + ActiveRecord::Base.connection_handler.establish_connection(old_pool.spec.to_hash) + end end def create_current(environment = env) @@ -111,12 +143,14 @@ module ActiveRecord def drop(*arguments) configuration = arguments.first - class_for_adapter(configuration['adapter']).new(*arguments).drop + class_for_adapter(configuration["adapter"]).new(*arguments).drop + $stdout.puts "Dropped database '#{configuration['database']}'" rescue ActiveRecord::NoDatabaseError $stderr.puts "Database '#{configuration['database']}' does not exist" rescue Exception => error - $stderr.puts error, *(error.backtrace) - $stderr.puts "Couldn't drop #{configuration['database']}" + $stderr.puts error + $stderr.puts "Couldn't drop database '#{configuration['database']}'" + raise end def drop_all @@ -132,11 +166,12 @@ module ActiveRecord def migrate verbose = ENV["VERBOSE"] ? ENV["VERBOSE"] == "true" : true version = ENV["VERSION"] ? ENV["VERSION"].to_i : nil - scope = ENV['SCOPE'] + scope = ENV["SCOPE"] verbose_was, Migration.verbose = Migration.verbose, verbose - Migrator.migrate(Migrator.migrations_paths, version) do |migration| + Migrator.migrate(migrations_paths, version) do |migration| scope.blank? || scope == migration.scope end + ActiveRecord::Base.clear_cache! ensure Migration.verbose = verbose_was end @@ -147,7 +182,7 @@ module ActiveRecord def charset(*arguments) configuration = arguments.first - class_for_adapter(configuration['adapter']).new(*arguments).charset + class_for_adapter(configuration["adapter"]).new(*arguments).charset end def collation_current(environment = env) @@ -156,11 +191,11 @@ module ActiveRecord def collation(*arguments) configuration = arguments.first - class_for_adapter(configuration['adapter']).new(*arguments).collation + class_for_adapter(configuration["adapter"]).new(*arguments).collation end def purge(configuration) - class_for_adapter(configuration['adapter']).new(configuration).purge + class_for_adapter(configuration["adapter"]).new(configuration).purge end def purge_all @@ -179,13 +214,13 @@ module ActiveRecord def structure_dump(*arguments) configuration = arguments.first filename = arguments.delete_at 1 - class_for_adapter(configuration['adapter']).new(*arguments).structure_dump(filename) + class_for_adapter(configuration["adapter"]).new(*arguments).structure_dump(filename, structure_dump_flags) end def structure_load(*arguments) configuration = arguments.first filename = arguments.delete_at 1 - class_for_adapter(configuration['adapter']).new(*arguments).structure_load(filename) + class_for_adapter(configuration["adapter"]).new(*arguments).structure_load(filename, structure_load_flags) end def load_schema(configuration, format = ActiveRecord::Base.schema_format, file = nil) # :nodoc: @@ -202,14 +237,8 @@ module ActiveRecord else raise ArgumentError, "unknown format #{format.inspect}" end - end - - def load_schema_for(*args) - ActiveSupport::Deprecation.warn(<<-MSG.squish) - This method was renamed to `#load_schema` and will be removed in the future. - Use `#load_schema` instead. - MSG - load_schema(*args) + ActiveRecord::InternalMetadata.create_table + ActiveRecord::InternalMetadata[:environment] = ActiveRecord::Migrator.current_environment end def schema_file(format = ActiveRecord::Base.schema_format) @@ -221,12 +250,6 @@ module ActiveRecord end end - def load_schema_current_if_exists(format = ActiveRecord::Base.schema_format, file = nil, environment = env) - if File.exist?(file || schema_file(format)) - load_schema_current(format, file, environment) - end - end - def load_schema_current(format = ActiveRecord::Base.schema_format, file = nil, environment = env) each_current_configuration(environment) { |configuration| load_schema configuration, format, file @@ -236,7 +259,7 @@ module ActiveRecord def check_schema_file(filename) unless File.exist?(filename) - message = %{#{filename} doesn't exist yet. Run `rake db:migrate` to create it, then try again.} + message = %{#{filename} doesn't exist yet. Run `rails db:migrate` to create it, then try again.} message << %{ If you do not intend to use a database, you should instead alter #{Rails.root}/config/application.rb to limit the frameworks that will be loaded.} if defined?(::Rails) Kernel.abort message end @@ -254,40 +277,39 @@ module ActiveRecord private - def class_for_adapter(adapter) - key = @tasks.keys.detect { |pattern| adapter[pattern] } - unless key - raise DatabaseNotSupported, "Rake tasks not supported by '#{adapter}' adapter" + def class_for_adapter(adapter) + key = @tasks.keys.detect { |pattern| adapter[pattern] } + unless key + raise DatabaseNotSupported, "Rake tasks not supported by '#{adapter}' adapter" + end + @tasks[key] end - @tasks[key] - end - def each_current_configuration(environment) - environments = [environment] - # add test environment only if no RAILS_ENV was specified. - environments << 'test' if environment == 'development' && ENV['RAILS_ENV'].nil? + def each_current_configuration(environment) + environments = [environment] + environments << "test" if environment == "development" - configurations = ActiveRecord::Base.configurations.values_at(*environments) - configurations.compact.each do |configuration| - yield configuration unless configuration['database'].blank? + configurations = ActiveRecord::Base.configurations.values_at(*environments) + configurations.compact.each do |configuration| + yield configuration unless configuration["database"].blank? + end end - end - def each_local_configuration - ActiveRecord::Base.configurations.each_value do |configuration| - next unless configuration['database'] + def each_local_configuration + ActiveRecord::Base.configurations.each_value do |configuration| + next unless configuration["database"] - if local_database?(configuration) - yield configuration - else - $stderr.puts "This task only modifies local databases. #{configuration['database']} is on a remote host." + if local_database?(configuration) + yield configuration + else + $stderr.puts "This task only modifies local databases. #{configuration['database']} is on a remote host." + end end end - end - def local_database?(configuration) - configuration['host'].blank? || LOCAL_HOSTS.include?(configuration['host']) - end + def local_database?(configuration) + configuration["host"].blank? || LOCAL_HOSTS.include?(configuration["host"]) + end end end end diff --git a/activerecord/lib/active_record/tasks/mysql_database_tasks.rb b/activerecord/lib/active_record/tasks/mysql_database_tasks.rb index 673386f0d9..920830b9cf 100644 --- a/activerecord/lib/active_record/tasks/mysql_database_tasks.rb +++ b/activerecord/lib/active_record/tasks/mysql_database_tasks.rb @@ -1,8 +1,6 @@ module ActiveRecord module Tasks # :nodoc: class MySQLDatabaseTasks # :nodoc: - DEFAULT_CHARSET = ENV['CHARSET'] || 'utf8' - DEFAULT_COLLATION = ENV['COLLATION'] || 'utf8_unicode_ci' ACCESS_DENIED_ERROR = 1045 delegate :connection, :establish_connection, to: ActiveRecord::Base @@ -13,38 +11,38 @@ module ActiveRecord def create establish_connection configuration_without_database - connection.create_database configuration['database'], creation_options + connection.create_database configuration["database"], creation_options establish_connection configuration rescue ActiveRecord::StatementInvalid => error - if /database exists/ === error.message + if error.message.include?("database exists") raise DatabaseAlreadyExists else raise end rescue error_class => error if error.respond_to?(:errno) && error.errno == ACCESS_DENIED_ERROR - $stdout.print error.error + $stdout.print error.message establish_connection root_configuration_without_database - connection.create_database configuration['database'], creation_options - if configuration['username'] != 'root' - connection.execute grant_statement.gsub(/\s+/, ' ').strip + connection.create_database configuration["database"], creation_options + if configuration["username"] != "root" + connection.execute grant_statement.gsub(/\s+/, " ").strip end establish_connection configuration else $stderr.puts error.inspect $stderr.puts "Couldn't create database for #{configuration.inspect}, #{creation_options.inspect}" - $stderr.puts "(If you set the charset manually, make sure you have a matching collation)" if configuration['encoding'] + $stderr.puts "(If you set the charset manually, make sure you have a matching collation)" if configuration["encoding"] end end def drop establish_connection configuration - connection.drop_database configuration['database'] + connection.drop_database configuration["database"] end def purge establish_connection configuration - connection.recreate_database configuration['database'], creation_options + connection.recreate_database configuration["database"], creation_options end def charset @@ -55,98 +53,102 @@ module ActiveRecord connection.collation end - def structure_dump(filename) - args = prepare_command_options('mysqldump') + def structure_dump(filename, extra_flags) + args = prepare_command_options args.concat(["--result-file", "#{filename}"]) args.concat(["--no-data"]) args.concat(["--routines"]) + args.concat(["--skip-comments"]) + args.concat(Array(extra_flags)) if extra_flags args.concat(["#{configuration['database']}"]) - unless Kernel.system(*args) - $stderr.puts "Could not dump the database structure. "\ - "Make sure `mysqldump` is in your PATH and check the command output for warnings." - end - end - def structure_load(filename) - args = prepare_command_options('mysql') - args.concat(['--execute', %{SET FOREIGN_KEY_CHECKS = 0; SOURCE #{filename}; SET FOREIGN_KEY_CHECKS = 1}]) - args.concat(["--database", "#{configuration['database']}"]) - Kernel.system(*args) + run_cmd("mysqldump", args, "dumping") end - private + def structure_load(filename, extra_flags) + args = prepare_command_options + args.concat(["--execute", %{SET FOREIGN_KEY_CHECKS = 0; SOURCE #{filename}; SET FOREIGN_KEY_CHECKS = 1}]) + args.concat(["--database", "#{configuration['database']}"]) + args.concat(Array(extra_flags)) if extra_flags - def configuration - @configuration + run_cmd("mysql", args, "loading") end - def configuration_without_database - configuration.merge('database' => nil) - end + private - def creation_options - Hash.new.tap do |options| - options[:charset] = configuration['encoding'] if configuration.include? 'encoding' - options[:collation] = configuration['collation'] if configuration.include? 'collation' + def configuration + @configuration + end - # Set default charset only when collation isn't set. - options[:charset] ||= DEFAULT_CHARSET unless options[:collation] + def configuration_without_database + configuration.merge("database" => nil) + end - # Set default collation only when charset is also default. - options[:collation] ||= DEFAULT_COLLATION if options[:charset] == DEFAULT_CHARSET + def creation_options + Hash.new.tap do |options| + options[:charset] = configuration["encoding"] if configuration.include? "encoding" + options[:collation] = configuration["collation"] if configuration.include? "collation" + end end - end - def error_class - if configuration['adapter'] =~ /jdbc/ - require 'active_record/railties/jdbcmysql_error' - ArJdbcMySQL::Error - elsif defined?(Mysql2) - Mysql2::Error - elsif defined?(Mysql) - Mysql::Error - else - StandardError + def error_class + if configuration["adapter"].include?("jdbc") + require "active_record/railties/jdbcmysql_error" + ArJdbcMySQL::Error + elsif defined?(Mysql2) + Mysql2::Error + else + StandardError + end end - end - def grant_statement - <<-SQL + def grant_statement + <<-SQL GRANT ALL PRIVILEGES ON #{configuration['database']}.* TO '#{configuration['username']}'@'localhost' IDENTIFIED BY '#{configuration['password']}' WITH GRANT OPTION; - SQL - end + SQL + end - def root_configuration_without_database - configuration_without_database.merge( - 'username' => 'root', - 'password' => root_password - ) - end + def root_configuration_without_database + configuration_without_database.merge( + "username" => "root", + "password" => root_password + ) + end - def root_password - $stdout.print "Please provide the root password for your MySQL installation\n>" - $stdin.gets.strip - end + def root_password + $stdout.print "Please provide the root password for your MySQL installation\n>" + $stdin.gets.strip + end - def prepare_command_options(command) - args = { - 'host' => '--host', - 'port' => '--port', - 'socket' => '--socket', - 'username' => '--user', - 'password' => '--password', - 'encoding' => '--default-character-set', - 'sslca' => '--ssl-ca', - 'sslcert' => '--ssl-cert', - 'sslcapath' => '--ssl-capath', - 'sslcipher' => '--ssh-cipher', - 'sslkey' => '--ssl-key' - }.map { |opt, arg| "#{arg}=#{configuration[opt]}" if configuration[opt] }.compact - - [command, *args] - end + def prepare_command_options + args = { + "host" => "--host", + "port" => "--port", + "socket" => "--socket", + "username" => "--user", + "password" => "--password", + "encoding" => "--default-character-set", + "sslca" => "--ssl-ca", + "sslcert" => "--ssl-cert", + "sslcapath" => "--ssl-capath", + "sslcipher" => "--ssl-cipher", + "sslkey" => "--ssl-key" + }.map { |opt, arg| "#{arg}=#{configuration[opt]}" if configuration[opt] }.compact + + args + end + + def run_cmd(cmd, args, action) + fail run_cmd_error(cmd, args, action) unless Kernel.system(cmd, *args) + end + + def run_cmd_error(cmd, args, action) + msg = "failed to execute: `#{cmd}`\n" + msg << "Please check the output above for any errors and make sure that `#{cmd}` is installed in your PATH and has proper permissions.\n\n" + msg + end end end end diff --git a/activerecord/lib/active_record/tasks/postgresql_database_tasks.rb b/activerecord/lib/active_record/tasks/postgresql_database_tasks.rb index d7da95c8a9..5155ced0e2 100644 --- a/activerecord/lib/active_record/tasks/postgresql_database_tasks.rb +++ b/activerecord/lib/active_record/tasks/postgresql_database_tasks.rb @@ -1,9 +1,8 @@ -require 'shellwords' - module ActiveRecord module Tasks # :nodoc: class PostgreSQLDatabaseTasks # :nodoc: - DEFAULT_ENCODING = ENV['CHARSET'] || 'utf8' + DEFAULT_ENCODING = ENV["CHARSET"] || "utf8" + ON_ERROR_STOP_1 = "ON_ERROR_STOP=1".freeze delegate :connection, :establish_connection, :clear_active_connections!, to: ActiveRecord::Base @@ -14,11 +13,11 @@ module ActiveRecord def create(master_established = false) establish_master_connection unless master_established - connection.create_database configuration['database'], - configuration.merge('encoding' => encoding) + connection.create_database configuration["database"], + configuration.merge("encoding" => encoding) establish_connection configuration rescue ActiveRecord::StatementInvalid => error - if /database .* already exists/ === error.message + if /database .* already exists/.match?(error.message) raise DatabaseAlreadyExists else raise @@ -27,7 +26,7 @@ module ActiveRecord def drop establish_master_connection - connection.drop_database configuration['database'] + connection.drop_database configuration["database"] end def charset @@ -44,55 +43,73 @@ module ActiveRecord create true end - def structure_dump(filename) + def structure_dump(filename, extra_flags) set_psql_env - search_path = case ActiveRecord::Base.dump_schemas - when :schema_search_path - configuration['schema_search_path'] - when :all - nil - when String - ActiveRecord::Base.dump_schemas - end + search_path = \ + case ActiveRecord::Base.dump_schemas + when :schema_search_path + configuration["schema_search_path"] + when :all + nil + when String + ActiveRecord::Base.dump_schemas + end + + args = ["-s", "-x", "-O", "-f", filename] + args.concat(Array(extra_flags)) if extra_flags unless search_path.blank? - search_path = search_path.split(",").map{|search_path_part| "--schema=#{Shellwords.escape(search_path_part.strip)}" }.join(" ") + args += search_path.split(",").map do |part| + "--schema=#{part.strip}" + end end - - command = "pg_dump -i -s -x -O -f #{Shellwords.escape(filename)} #{search_path} #{Shellwords.escape(configuration['database'])}" - raise 'Error dumping database' unless Kernel.system(command) - + args << configuration["database"] + run_cmd("pg_dump", args, "dumping") File.open(filename, "a") { |f| f << "SET search_path TO #{connection.schema_search_path};\n\n" } end - def structure_load(filename) + def structure_load(filename, extra_flags) set_psql_env - Kernel.system("psql -X -q -f #{Shellwords.escape(filename)} #{configuration['database']}") + args = ["-v", ON_ERROR_STOP_1, "-q", "-f", filename] + args.concat(Array(extra_flags)) if extra_flags + args << configuration["database"] + run_cmd("psql", args, "loading") end private - def configuration - @configuration - end + def configuration + @configuration + end - def encoding - configuration['encoding'] || DEFAULT_ENCODING - end + def encoding + configuration["encoding"] || DEFAULT_ENCODING + end - def establish_master_connection - establish_connection configuration.merge( - 'database' => 'postgres', - 'schema_search_path' => 'public' - ) - end + def establish_master_connection + establish_connection configuration.merge( + "database" => "postgres", + "schema_search_path" => "public" + ) + end - def set_psql_env - ENV['PGHOST'] = configuration['host'] if configuration['host'] - ENV['PGPORT'] = configuration['port'].to_s if configuration['port'] - ENV['PGPASSWORD'] = configuration['password'].to_s if configuration['password'] - ENV['PGUSER'] = configuration['username'].to_s if configuration['username'] - end + def set_psql_env + ENV["PGHOST"] = configuration["host"] if configuration["host"] + ENV["PGPORT"] = configuration["port"].to_s if configuration["port"] + ENV["PGPASSWORD"] = configuration["password"].to_s if configuration["password"] + ENV["PGUSER"] = configuration["username"].to_s if configuration["username"] + end + + def run_cmd(cmd, args, action) + fail run_cmd_error(cmd, args, action) unless Kernel.system(cmd, *args) + end + + def run_cmd_error(cmd, args, action) + msg = "failed to execute:\n" + msg << "#{cmd} #{args.join(' ')}\n\n" + msg << "Please check the output above for any errors and make sure that `#{cmd}` is installed in your PATH and has proper permissions.\n\n" + msg + end end end end diff --git a/activerecord/lib/active_record/tasks/sqlite_database_tasks.rb b/activerecord/lib/active_record/tasks/sqlite_database_tasks.rb index 9ab64d0325..1f756c2979 100644 --- a/activerecord/lib/active_record/tasks/sqlite_database_tasks.rb +++ b/activerecord/lib/active_record/tasks/sqlite_database_tasks.rb @@ -8,22 +8,26 @@ module ActiveRecord end def create - raise DatabaseAlreadyExists if File.exist?(configuration['database']) + raise DatabaseAlreadyExists if File.exist?(configuration["database"]) establish_connection configuration connection end def drop - require 'pathname' - path = Pathname.new configuration['database'] + require "pathname" + path = Pathname.new configuration["database"] file = path.absolute? ? path.to_s : File.join(root, path) - FileUtils.rm(file) if File.exist?(file) + FileUtils.rm(file) + rescue Errno::ENOENT => error + raise NoDatabaseError.new(error.message) end def purge drop + rescue NoDatabaseError + ensure create end @@ -31,25 +35,27 @@ module ActiveRecord connection.encoding end - def structure_dump(filename) - dbfile = configuration['database'] - `sqlite3 #{dbfile} .schema > #{filename}` + def structure_dump(filename, extra_flags) + dbfile = configuration["database"] + flags = extra_flags.join(" ") if extra_flags + `sqlite3 #{flags} #{dbfile} .schema > #{filename}` end - def structure_load(filename) - dbfile = configuration['database'] - `sqlite3 #{dbfile} < "#{filename}"` + def structure_load(filename, extra_flags) + dbfile = configuration["database"] + flags = extra_flags.join(" ") if extra_flags + `sqlite3 #{flags} #{dbfile} < "#{filename}"` end private - def configuration - @configuration - end + def configuration + @configuration + end - def root - @root - end + def root + @root + end end end end diff --git a/activerecord/lib/active_record/timestamp.rb b/activerecord/lib/active_record/timestamp.rb index 20e4235788..63100e38a1 100644 --- a/activerecord/lib/active_record/timestamp.rb +++ b/activerecord/lib/active_record/timestamp.rb @@ -1,5 +1,5 @@ module ActiveRecord - # = Active Record Timestamp + # = Active Record \Timestamp # # Active Record automatically timestamps create and update operations if the # table has fields named <tt>created_at/created_on</tt> or @@ -15,14 +15,25 @@ module ActiveRecord # # == Time Zone aware attributes # - # By default, ActiveRecord::Base keeps all the datetime columns time zone aware by executing following code. + # Active Record keeps all the <tt>datetime</tt> and <tt>time</tt> columns + # timezone aware. By default, these values are stored in the database as UTC + # and converted back to the current <tt>Time.zone</tt> when pulled from the database. # - # config.active_record.time_zone_aware_attributes = true + # This feature can be turned off completely by setting: # - # This feature can easily be turned off by assigning value <tt>false</tt> . + # config.active_record.time_zone_aware_attributes = false # - # If your attributes are time zone aware and you desire to skip time zone conversion to the current Time.zone - # when reading certain attributes then you can do following: + # You can also specify that only <tt>datetime</tt> columns should be time-zone + # aware (while <tt>time</tt> should not) by setting: + # + # ActiveRecord::Base.time_zone_aware_types = [:datetime] + # + # You can also add database specific timezone aware types. For example, for PostgreSQL: + # + # ActiveRecord::Base.time_zone_aware_types += [:tsrange, :tstzrange] + # + # Finally, you can indicate specific attributes of a model for which time zone + # conversion should not applied, for instance by setting: # # class Topic < ActiveRecord::Base # self.skip_time_zone_conversion_for_attributes = [:written_on] @@ -43,7 +54,7 @@ module ActiveRecord private def _create_record - if self.record_timestamps + if record_timestamps current_time = current_time_from_proper_timezone all_timestamp_attributes.each do |column| @@ -63,7 +74,7 @@ module ActiveRecord timestamp_attributes_for_update_in_model.each do |column| column = column.to_s - next if attribute_changed?(column) + next if will_save_change_to_attribute?(column) write_attribute(column, current_time) end end @@ -71,7 +82,7 @@ module ActiveRecord end def should_record_timestamps? - self.record_timestamps && (!partial_writes? || changed?) + record_timestamps && (!partial_writes? || has_changes_to_save?) end def timestamp_attributes_for_create_in_model diff --git a/activerecord/lib/active_record/touch_later.rb b/activerecord/lib/active_record/touch_later.rb index 4352a0ffea..cacde9c881 100644 --- a/activerecord/lib/active_record/touch_later.rb +++ b/activerecord/lib/active_record/touch_later.rb @@ -8,7 +8,12 @@ module ActiveRecord end def touch_later(*names) # :nodoc: - raise ActiveRecordError, "cannot touch on a new record object" unless persisted? + unless persisted? + raise ActiveRecordError, <<-MSG.squish + cannot touch on a new or destroyed record object. Consider using + persisted?, new_record?, or destroyed? before touching + MSG + end @_defer_touch_attrs ||= timestamp_attributes_for_update_in_model @_defer_touch_attrs |= names @@ -16,6 +21,13 @@ module ActiveRecord surreptitiously_touch @_defer_touch_attrs self.class.connection.add_transaction_record self + + # touch the parents as we are not calling the after_save callbacks + self.class.reflect_on_all_associations(:belongs_to).each do |r| + if touch = r.options[:touch] + ActiveRecord::Associations::Builder::BelongsTo.touch_record(self, changes_to_save, r.foreign_key, r.name, touch, :touch_later) + end + end end def touch(*names, time: nil) # :nodoc: @@ -26,6 +38,7 @@ module ActiveRecord end private + def surreptitiously_touch(attrs) attrs.each { |attr| write_attribute attr, @_touch_time } clear_attribute_changes attrs @@ -33,9 +46,8 @@ module ActiveRecord def touch_deferred_attributes if has_defer_touch_attrs? && persisted? - @_touching_delayed_records = true touch(*@_defer_touch_attrs, time: @_touch_time) - @_touching_delayed_records, @_defer_touch_attrs, @_touch_time = nil, nil, nil + @_defer_touch_attrs, @_touch_time = nil, nil end end @@ -43,8 +55,8 @@ module ActiveRecord defined?(@_defer_touch_attrs) && @_defer_touch_attrs.present? end - def touching_delayed_records? - defined?(@_touching_delayed_records) && @_touching_delayed_records + def belongs_to_touch_method + :touch_later end end end diff --git a/activerecord/lib/active_record/transactions.rb b/activerecord/lib/active_record/transactions.rb index 6f2def0df1..56b75540e3 100644 --- a/activerecord/lib/active_record/transactions.rb +++ b/activerecord/lib/active_record/transactions.rb @@ -11,15 +11,16 @@ module ActiveRecord :before_commit_without_transaction_enrollment, :commit_without_transaction_enrollment, :rollback_without_transaction_enrollment, + terminator: deprecated_false_terminator, scope: [:kind, :name] end # = Active Record Transactions # - # Transactions are protective blocks where SQL statements are only permanent + # \Transactions are protective blocks where SQL statements are only permanent # if they can all succeed as one atomic action. The classic example is a # transfer between two accounts where you can only have a deposit if the - # withdrawal succeeded and vice versa. Transactions enforce the integrity of + # withdrawal succeeded and vice versa. \Transactions enforce the integrity of # the database and guard the data against program errors or database # break-downs. So basically you should use transaction blocks whenever you # have a number of statements that must be executed together or not at all. @@ -39,20 +40,20 @@ module ActiveRecord # # == Different Active Record classes in a single transaction # - # Though the transaction class method is called on some Active Record class, + # Though the #transaction class method is called on some Active Record class, # the objects within the transaction block need not all be instances of # that class. This is because transactions are per-database connection, not # per-model. # # In this example a +balance+ record is transactionally saved even - # though +transaction+ is called on the +Account+ class: + # though #transaction is called on the +Account+ class: # # Account.transaction do # balance.save! # account.save! # end # - # The +transaction+ method is also available as a model instance method. + # The #transaction method is also available as a model instance method. # For example, you can also do this: # # balance.transaction do @@ -79,7 +80,8 @@ module ActiveRecord # # == +save+ and +destroy+ are automatically wrapped in a transaction # - # Both +save+ and +destroy+ come wrapped in a transaction that ensures + # Both {#save}[rdoc-ref:Persistence#save] and + # {#destroy}[rdoc-ref:Persistence#destroy] come wrapped in a transaction that ensures # that whatever you do in validations or callbacks will happen under its # protected cover. So you can use validations to check for values that # the transaction depends on or you can raise exceptions in the callbacks @@ -88,7 +90,7 @@ module ActiveRecord # As a consequence changes to the database are not seen outside your connection # until the operation is complete. For example, if you try to update the index # of a search engine in +after_save+ the indexer won't see the updated record. - # The +after_commit+ callback is the only one that is triggered once the update + # The #after_commit callback is the only one that is triggered once the update # is committed. See below. # # == Exception handling and rolling back @@ -97,11 +99,11 @@ module ActiveRecord # be propagated (after triggering the ROLLBACK), so you should be ready to # catch those in your application code. # - # One exception is the <tt>ActiveRecord::Rollback</tt> exception, which will trigger + # One exception is the ActiveRecord::Rollback exception, which will trigger # a ROLLBACK when raised, but not be re-raised by the transaction block. # - # *Warning*: one should not catch <tt>ActiveRecord::StatementInvalid</tt> exceptions - # inside a transaction block. <tt>ActiveRecord::StatementInvalid</tt> exceptions indicate that an + # *Warning*: one should not catch ActiveRecord::StatementInvalid exceptions + # inside a transaction block. ActiveRecord::StatementInvalid exceptions indicate that an # error occurred at the database level, for example when a unique constraint # is violated. On some database systems, such as PostgreSQL, database errors # inside a transaction cause the entire transaction to become unusable @@ -127,11 +129,11 @@ module ActiveRecord # end # # One should restart the entire transaction if an - # <tt>ActiveRecord::StatementInvalid</tt> occurred. + # ActiveRecord::StatementInvalid occurred. # # == Nested transactions # - # +transaction+ calls can be nested. By default, this makes all database + # #transaction calls can be nested. By default, this makes all database # statements in the nested transaction block become part of the parent # transaction. For example, the following behavior may be surprising: # @@ -143,7 +145,7 @@ module ActiveRecord # end # end # - # creates both "Kotori" and "Nemu". Reason is the <tt>ActiveRecord::Rollback</tt> + # creates both "Kotori" and "Nemu". Reason is the ActiveRecord::Rollback # exception in the nested block does not issue a ROLLBACK. Since these exceptions # are captured in transaction blocks, the parent block does not see it and the # real transaction is committed. @@ -167,28 +169,28 @@ module ActiveRecord # writing, the only database that we're aware of that supports true nested # transactions, is MS-SQL. Because of this, Active Record emulates nested # transactions by using savepoints on MySQL and PostgreSQL. See - # http://dev.mysql.com/doc/refman/5.6/en/savepoint.html + # http://dev.mysql.com/doc/refman/5.7/en/savepoint.html # for more information about savepoints. # - # === Callbacks + # === \Callbacks # # There are two types of callbacks associated with committing and rolling back transactions: - # +after_commit+ and +after_rollback+. + # #after_commit and #after_rollback. # - # +after_commit+ callbacks are called on every record saved or destroyed within a - # transaction immediately after the transaction is committed. +after_rollback+ callbacks + # #after_commit callbacks are called on every record saved or destroyed within a + # transaction immediately after the transaction is committed. #after_rollback callbacks # are called on every record saved or destroyed within a transaction immediately after the # transaction or savepoint is rolled back. # # These callbacks are useful for interacting with other systems since you will be guaranteed # that the callback is only executed when the database is in a permanent state. For example, - # +after_commit+ is a good spot to put in a hook to clearing a cache since clearing it from + # #after_commit is a good spot to put in a hook to clearing a cache since clearing it from # within a transaction could trigger the cache to be regenerated before the database is updated. # # === Caveats # - # If you're on MySQL, then do not use DDL operations in nested transactions - # blocks that are emulated with savepoints. That is, do not execute statements + # If you're on MySQL, then do not use Data Definition Language(DDL) operations in nested + # transactions blocks that are emulated with savepoints. That is, do not execute statements # like 'CREATE TABLE' inside such blocks. This is because MySQL automatically # releases all savepoints upon executing a DDL operation. When +transaction+ # is finished and tries to release the savepoint it created earlier, a @@ -204,9 +206,8 @@ module ActiveRecord # # Note that "TRUNCATE" is also a MySQL DDL statement! module ClassMethods - # See ActiveRecord::Transactions::ClassMethods for detailed documentation. + # See the ConnectionAdapters::DatabaseStatements#transaction API docs. def transaction(options = {}, &block) - # See the ConnectionAdapters::DatabaseStatements#transaction API docs. connection.transaction(options, &block) end @@ -232,9 +233,27 @@ module ActiveRecord set_callback(:commit, :after, *args, &block) end + # Shortcut for <tt>after_commit :hook, on: :create</tt>. + def after_create_commit(*args, &block) + set_options_for_callbacks!(args, on: :create) + set_callback(:commit, :after, *args, &block) + end + + # Shortcut for <tt>after_commit :hook, on: :update</tt>. + def after_update_commit(*args, &block) + set_options_for_callbacks!(args, on: :update) + set_callback(:commit, :after, *args, &block) + end + + # Shortcut for <tt>after_commit :hook, on: :destroy</tt>. + def after_destroy_commit(*args, &block) + set_options_for_callbacks!(args, on: :destroy) + set_callback(:commit, :after, *args, &block) + end + # This callback is called after a create, update, or destroy are rolled back. # - # Please check the documentation of +after_commit+ for options. + # Please check the documentation of #after_commit for options. def after_rollback(*args, &block) set_options_for_callbacks!(args) set_callback(:rollback, :after, *args, &block) @@ -255,33 +274,25 @@ module ActiveRecord set_callback(:rollback_without_transaction_enrollment, :after, *args, &block) end - def raise_in_transactional_callbacks - ActiveSupport::Deprecation.warn('ActiveRecord::Base.raise_in_transactional_callbacks is deprecated and will be removed without replacement.') - true - end - - def raise_in_transactional_callbacks=(value) - ActiveSupport::Deprecation.warn('ActiveRecord::Base.raise_in_transactional_callbacks= is deprecated, has no effect and will be removed without replacement.') - value - end - private - def set_options_for_callbacks!(args) - options = args.last - if options.is_a?(Hash) && options[:on] - fire_on = Array(options[:on]) - assert_valid_transaction_action(fire_on) - options[:if] = Array(options[:if]) - options[:if] << "transaction_include_any_action?(#{fire_on})" + def set_options_for_callbacks!(args, enforced_options = {}) + options = args.extract_options!.merge!(enforced_options) + args << options + + if options[:on] + fire_on = Array(options[:on]) + assert_valid_transaction_action(fire_on) + options[:if] = Array(options[:if]) + options[:if] << "transaction_include_any_action?(#{fire_on})" + end end - end - def assert_valid_transaction_action(actions) - if (actions - ACTIONS).any? - raise ArgumentError, ":on conditions for after_commit and after_rollback callbacks have to be one of #{ACTIONS}" + def assert_valid_transaction_action(actions) + if (actions - ACTIONS).any? + raise ArgumentError, ":on conditions for after_commit and after_rollback callbacks have to be one of #{ACTIONS}" + end end - end end # See ActiveRecord::Transactions::ClassMethods for detailed documentation. @@ -319,36 +330,36 @@ module ActiveRecord end def before_committed! # :nodoc: - run_callbacks :before_commit_without_transaction_enrollment - run_callbacks :before_commit + _run_before_commit_without_transaction_enrollment_callbacks + _run_before_commit_callbacks end - # Call the +after_commit+ callbacks. + # Call the #after_commit callbacks. # # Ensure that it is not called if the object was never persisted (failed create), # but call it after the commit of a destroyed object. def committed!(should_run_callbacks: true) #:nodoc: if should_run_callbacks && destroyed? || persisted? - run_callbacks :commit_without_transaction_enrollment - run_callbacks :commit + _run_commit_without_transaction_enrollment_callbacks + _run_commit_callbacks end ensure force_clear_transaction_record_state end - # Call the +after_rollback+ callbacks. The +force_restore_state+ argument indicates if the record + # Call the #after_rollback callbacks. The +force_restore_state+ argument indicates if the record # state should be rolled back to the beginning or just to the last savepoint. def rolledback!(force_restore_state: false, should_run_callbacks: true) #:nodoc: if should_run_callbacks - run_callbacks :rollback - run_callbacks :rollback_without_transaction_enrollment + _run_rollback_callbacks + _run_rollback_without_transaction_enrollment_callbacks end ensure restore_transaction_record_state(force_restore_state) clear_transaction_record_state end - # Add the record to the current transaction so that the +after_rollback+ and +after_commit+ callbacks + # Add the record to the current transaction so that the #after_rollback and #after_commit callbacks # can be called. def add_to_transaction if has_transactional_callbacks? @@ -380,105 +391,108 @@ module ActiveRecord raise ActiveRecord::Rollback unless status end status + ensure + if @transaction_state && @transaction_state.committed? + clear_transaction_record_state + end end - protected - - # Save the new record state and id of a record so it can be restored later if a transaction fails. - def remember_transaction_record_state #:nodoc: - @_start_transaction_state[:id] = id - @_start_transaction_state.reverse_merge!( - new_record: @new_record, - destroyed: @destroyed, - frozen?: frozen?, - ) - @_start_transaction_state[:level] = (@_start_transaction_state[:level] || 0) + 1 - end + private - # Clear the new record state and id of a record. - def clear_transaction_record_state #:nodoc: - @_start_transaction_state[:level] = (@_start_transaction_state[:level] || 0) - 1 - force_clear_transaction_record_state if @_start_transaction_state[:level] < 1 - end + # Save the new record state and id of a record so it can be restored later if a transaction fails. + def remember_transaction_record_state + @_start_transaction_state[:id] = id + @_start_transaction_state.reverse_merge!( + new_record: @new_record, + destroyed: @destroyed, + frozen?: frozen?, + ) + @_start_transaction_state[:level] = (@_start_transaction_state[:level] || 0) + 1 + end - # Force to clear the transaction record state. - def force_clear_transaction_record_state #:nodoc: - @_start_transaction_state.clear - end + # Clear the new record state and id of a record. + def clear_transaction_record_state + @_start_transaction_state[:level] = (@_start_transaction_state[:level] || 0) - 1 + force_clear_transaction_record_state if @_start_transaction_state[:level] < 1 + end + + # Force to clear the transaction record state. + def force_clear_transaction_record_state + @_start_transaction_state.clear + end - # Restore the new record state and id of a record that was previously saved by a call to save_record_state. - def restore_transaction_record_state(force = false) #:nodoc: - unless @_start_transaction_state.empty? - transaction_level = (@_start_transaction_state[:level] || 0) - 1 - if transaction_level < 1 || force - restore_state = @_start_transaction_state - thaw - @new_record = restore_state[:new_record] - @destroyed = restore_state[:destroyed] - pk = self.class.primary_key - if pk && read_attribute(pk) != restore_state[:id] - write_attribute(pk, restore_state[:id]) + # Restore the new record state and id of a record that was previously saved by a call to save_record_state. + def restore_transaction_record_state(force = false) + unless @_start_transaction_state.empty? + transaction_level = (@_start_transaction_state[:level] || 0) - 1 + if transaction_level < 1 || force + restore_state = @_start_transaction_state + thaw + @new_record = restore_state[:new_record] + @destroyed = restore_state[:destroyed] + pk = self.class.primary_key + if pk && read_attribute(pk) != restore_state[:id] + write_attribute(pk, restore_state[:id]) + end + freeze if restore_state[:frozen?] end - freeze if restore_state[:frozen?] end end - end - # Determine if a record was created or destroyed in a transaction. State should be one of :new_record or :destroyed. - def transaction_record_state(state) #:nodoc: - @_start_transaction_state[state] - end + # Determine if a record was created or destroyed in a transaction. State should be one of :new_record or :destroyed. + def transaction_record_state(state) + @_start_transaction_state[state] + end - # Determine if a transaction included an action for :create, :update, or :destroy. Used in filtering callbacks. - def transaction_include_any_action?(actions) #:nodoc: - actions.any? do |action| - case action - when :create - transaction_record_state(:new_record) - when :destroy - destroyed? - when :update - !(transaction_record_state(:new_record) || destroyed?) + # Determine if a transaction included an action for :create, :update, or :destroy. Used in filtering callbacks. + def transaction_include_any_action?(actions) + actions.any? do |action| + case action + when :create + transaction_record_state(:new_record) + when :destroy + defined?(@_trigger_destroy_callback) && @_trigger_destroy_callback + when :update + !(transaction_record_state(:new_record) || destroyed?) && + (defined?(@_trigger_update_callback) && @_trigger_update_callback) + end end end - end - private - - def set_transaction_state(state) # :nodoc: - @transaction_state = state - end + def set_transaction_state(state) + @transaction_state = state + end - def has_transactional_callbacks? # :nodoc: - !_rollback_callbacks.empty? || !_commit_callbacks.empty? || !_before_commit_callbacks.empty? - end + def has_transactional_callbacks? + !_rollback_callbacks.empty? || !_commit_callbacks.empty? || !_before_commit_callbacks.empty? + end - # Updates the attributes on this particular ActiveRecord object so that - # if it's associated with a transaction, then the state of the ActiveRecord - # object will be updated to reflect the current state of the transaction - # - # The @transaction_state variable stores the states of the associated - # transaction. This relies on the fact that a transaction can only be in - # one rollback or commit (otherwise a list of states would be required) - # Each ActiveRecord object inside of a transaction carries that transaction's - # TransactionState. - # - # This method checks to see if the ActiveRecord object's state reflects - # the TransactionState, and rolls back or commits the ActiveRecord object - # as appropriate. - # - # Since ActiveRecord objects can be inside multiple transactions, this - # method recursively goes through the parent of the TransactionState and - # checks if the ActiveRecord object reflects the state of the object. - def sync_with_transaction_state - update_attributes_from_transaction_state(@transaction_state) - end + # Updates the attributes on this particular Active Record object so that + # if it's associated with a transaction, then the state of the Active Record + # object will be updated to reflect the current state of the transaction. + # + # The +@transaction_state+ variable stores the states of the associated + # transaction. This relies on the fact that a transaction can only be in + # one rollback or commit (otherwise a list of states would be required). + # Each Active Record object inside of a transaction carries that transaction's + # TransactionState. + # + # This method checks to see if the ActiveRecord object's state reflects + # the TransactionState, and rolls back or commits the Active Record object + # as appropriate. + # + # Since Active Record objects can be inside multiple transactions, this + # method recursively goes through the parent of the TransactionState and + # checks if the Active Record object reflects the state of the object. + def sync_with_transaction_state + update_attributes_from_transaction_state(@transaction_state) + end - def update_attributes_from_transaction_state(transaction_state) - if transaction_state && transaction_state.finalized? - restore_transaction_record_state if transaction_state.rolledback? - clear_transaction_record_state + def update_attributes_from_transaction_state(transaction_state) + if transaction_state && transaction_state.finalized? + restore_transaction_record_state if transaction_state.rolledback? + clear_transaction_record_state + end end - end end end diff --git a/activerecord/lib/active_record/type.rb b/activerecord/lib/active_record/type.rb index 2c0cda69d0..4f632660a8 100644 --- a/activerecord/lib/active_record/type.rb +++ b/activerecord/lib/active_record/type.rb @@ -1,24 +1,20 @@ -require 'active_record/type/helpers' -require 'active_record/type/value' +require "active_model/type" -require 'active_record/type/big_integer' -require 'active_record/type/binary' -require 'active_record/type/boolean' -require 'active_record/type/date' -require 'active_record/type/date_time' -require 'active_record/type/decimal' -require 'active_record/type/decimal_without_scale' -require 'active_record/type/float' -require 'active_record/type/integer' -require 'active_record/type/serialized' -require 'active_record/type/string' -require 'active_record/type/text' -require 'active_record/type/time' -require 'active_record/type/unsigned_integer' +require "active_record/type/internal/abstract_json" +require "active_record/type/internal/timezone" -require 'active_record/type/adapter_specific_registry' -require 'active_record/type/type_map' -require 'active_record/type/hash_lookup_type_map' +require "active_record/type/date" +require "active_record/type/date_time" +require "active_record/type/decimal_without_scale" +require "active_record/type/time" +require "active_record/type/text" +require "active_record/type/unsigned_integer" + +require "active_record/type/serialized" +require "active_record/type/adapter_specific_registry" + +require "active_record/type/type_map" +require "active_record/type/hash_lookup_type_map" module ActiveRecord module Type @@ -29,13 +25,13 @@ module ActiveRecord delegate :add_modifier, to: :registry # Add a new type to the registry, allowing it to be referenced as a - # symbol by ActiveRecord::Attributes::ClassMethods#attribute. If your - # type is only meant to be used with a specific database adapter, you can - # do so by passing +adapter: :postgresql+. If your type has the same + # symbol by {ActiveRecord::Base.attribute}[rdoc-ref:Attributes::ClassMethods#attribute]. + # If your type is only meant to be used with a specific database adapter, you can + # do so by passing <tt>adapter: :postgresql</tt>. If your type has the same # name as a native type for the current adapter, an exception will be - # raised unless you specify an +:override+ option. +override: true+ will - # cause your type to be used instead of the native type. +override: - # false+ will cause the native type to be used over yours if one exists. + # raised unless you specify an +:override+ option. <tt>override: true</tt> will + # cause your type to be used instead of the native type. <tt>override: + # false</tt> will cause the native type to be used over yours if one exists. def register(type_name, klass = nil, **options, &block) registry.register(type_name, klass, **options, &block) end @@ -44,6 +40,10 @@ module ActiveRecord registry.lookup(*args, adapter: adapter, **kwargs) end + def default_value # :nodoc: + @default_value ||= Value.new + end + private def current_adapter_name @@ -51,11 +51,21 @@ module ActiveRecord end end + Helpers = ActiveModel::Type::Helpers + BigInteger = ActiveModel::Type::BigInteger + Binary = ActiveModel::Type::Binary + Boolean = ActiveModel::Type::Boolean + Decimal = ActiveModel::Type::Decimal + Float = ActiveModel::Type::Float + Integer = ActiveModel::Type::Integer + String = ActiveModel::Type::String + Value = ActiveModel::Type::Value + register(:big_integer, Type::BigInteger, override: false) register(:binary, Type::Binary, override: false) register(:boolean, Type::Boolean, override: false) register(:date, Type::Date, override: false) - register(:date_time, Type::DateTime, override: false) + register(:datetime, Type::DateTime, override: false) register(:decimal, Type::Decimal, override: false) register(:float, Type::Float, override: false) register(:integer, Type::Integer, override: false) diff --git a/activerecord/lib/active_record/type/adapter_specific_registry.rb b/activerecord/lib/active_record/type/adapter_specific_registry.rb index 5f71b3cb94..7cc866f7a7 100644 --- a/activerecord/lib/active_record/type/adapter_specific_registry.rb +++ b/activerecord/lib/active_record/type/adapter_specific_registry.rb @@ -1,35 +1,24 @@ +require "active_model/type/registry" + module ActiveRecord # :stopdoc: module Type - class AdapterSpecificRegistry - def initialize - @registrations = [] - end - - def register(type_name, klass = nil, **options, &block) - block ||= proc { |_, *args| klass.new(*args) } - registrations << Registration.new(type_name, block, **options) - end - - def lookup(symbol, *args) - registration = registrations - .select { |r| r.matches?(symbol, *args) } - .max - - if registration - registration.call(self, symbol, *args) - else - raise ArgumentError, "Unknown type #{symbol.inspect}" - end - end - + class AdapterSpecificRegistry < ActiveModel::Type::Registry def add_modifier(options, klass, **args) registrations << DecorationRegistration.new(options, klass, **args) end - protected + private - attr_reader :registrations + def registration_klass + Registration + end + + def find_registration(symbol, *args) + registrations + .select { |registration| registration.matches?(symbol, *args) } + .max + end end class Registration @@ -61,44 +50,46 @@ module ActiveRecord priority <=> other.priority end + # TODO Change this to private once we've dropped Ruby 2.2 support. + # Workaround for Ruby 2.2 "private attribute?" warning. protected - attr_reader :name, :block, :adapter, :override - - def priority - result = 0 - if adapter - result |= 1 - end - if override - result |= 2 + attr_reader :name, :block, :adapter, :override + + def priority + result = 0 + if adapter + result |= 1 + end + if override + result |= 2 + end + result end - result - end - def priority_except_adapter - priority & 0b111111100 - end + def priority_except_adapter + priority & 0b111111100 + end private - def matches_adapter?(adapter: nil, **) - (self.adapter.nil? || adapter == self.adapter) - end + def matches_adapter?(adapter: nil, **) + (self.adapter.nil? || adapter == self.adapter) + end - def conflicts_with?(other) - same_priority_except_adapter?(other) && - has_adapter_conflict?(other) - end + def conflicts_with?(other) + same_priority_except_adapter?(other) && + has_adapter_conflict?(other) + end - def same_priority_except_adapter?(other) - priority_except_adapter == other.priority_except_adapter - end + def same_priority_except_adapter?(other) + priority_except_adapter == other.priority_except_adapter + end - def has_adapter_conflict?(other) - (override.nil? && other.adapter) || - (adapter && other.override.nil?) - end + def has_adapter_conflict?(other) + (override.nil? && other.adapter) || + (adapter && other.override.nil?) + end end class DecorationRegistration < Registration @@ -121,22 +112,23 @@ module ActiveRecord super | 4 end + # TODO Change this to private once we've dropped Ruby 2.2 support. + # Workaround for Ruby 2.2 "private attribute?" warning. protected - attr_reader :options, :klass + attr_reader :options, :klass private - def matches_options?(**kwargs) - options.all? do |key, value| - kwargs[key] == value + def matches_options?(**kwargs) + options.all? do |key, value| + kwargs[key] == value + end end - end end end class TypeConflictError < StandardError end - # :startdoc: end diff --git a/activerecord/lib/active_record/type/big_integer.rb b/activerecord/lib/active_record/type/big_integer.rb deleted file mode 100644 index 0c72d8914f..0000000000 --- a/activerecord/lib/active_record/type/big_integer.rb +++ /dev/null @@ -1,13 +0,0 @@ -require 'active_record/type/integer' - -module ActiveRecord - module Type - class BigInteger < Integer # :nodoc: - private - - def max_value - ::Float::INFINITY - end - end - end -end diff --git a/activerecord/lib/active_record/type/binary.rb b/activerecord/lib/active_record/type/binary.rb deleted file mode 100644 index 0baf8c63ad..0000000000 --- a/activerecord/lib/active_record/type/binary.rb +++ /dev/null @@ -1,50 +0,0 @@ -module ActiveRecord - module Type - class Binary < Value # :nodoc: - def type - :binary - end - - def binary? - true - end - - def cast(value) - if value.is_a?(Data) - value.to_s - else - super - end - end - - def serialize(value) - return if value.nil? - Data.new(super) - end - - def changed_in_place?(raw_old_value, value) - old_value = deserialize(raw_old_value) - old_value != value - end - - class Data # :nodoc: - def initialize(value) - @value = value.to_s - end - - def to_s - @value - end - alias_method :to_str, :to_s - - def hex - @value.unpack('H*')[0] - end - - def ==(other) - other == to_s || super - end - end - end - end -end diff --git a/activerecord/lib/active_record/type/boolean.rb b/activerecord/lib/active_record/type/boolean.rb deleted file mode 100644 index f6a75512fd..0000000000 --- a/activerecord/lib/active_record/type/boolean.rb +++ /dev/null @@ -1,19 +0,0 @@ -module ActiveRecord - module Type - class Boolean < Value # :nodoc: - def type - :boolean - end - - private - - def cast_value(value) - if value == '' - nil - else - !ConnectionAdapters::Column::FALSE_VALUES.include?(value) - end - end - end - end -end diff --git a/activerecord/lib/active_record/type/date.rb b/activerecord/lib/active_record/type/date.rb index 3ceab59ebb..ccafed054e 100644 --- a/activerecord/lib/active_record/type/date.rb +++ b/activerecord/lib/active_record/type/date.rb @@ -1,49 +1,7 @@ module ActiveRecord module Type - class Date < Value # :nodoc: - include Helpers::AcceptsMultiparameterTime.new - - def type - :date - end - - def type_cast_for_schema(value) - "'#{value.to_s(:db)}'" - end - - private - - def cast_value(value) - if value.is_a?(::String) - return if value.empty? - fast_string_to_date(value) || fallback_string_to_date(value) - elsif value.respond_to?(:to_date) - value.to_date - else - value - end - end - - def fast_string_to_date(string) - if string =~ ConnectionAdapters::Column::Format::ISO_DATE - new_date $1.to_i, $2.to_i, $3.to_i - end - end - - def fallback_string_to_date(string) - new_date(*::Date._parse(string, false).values_at(:year, :mon, :mday)) - end - - def new_date(year, mon, mday) - if year && year != 0 - ::Date.new(year, mon, mday) rescue nil - end - end - - def value_from_multiparameter_assignment(*) - time = super - time && time.to_date - end + class Date < ActiveModel::Type::Date + include Internal::Timezone end end end diff --git a/activerecord/lib/active_record/type/date_time.rb b/activerecord/lib/active_record/type/date_time.rb index a5199959b9..1fb9380ecd 100644 --- a/activerecord/lib/active_record/type/date_time.rb +++ b/activerecord/lib/active_record/type/date_time.rb @@ -1,44 +1,7 @@ module ActiveRecord module Type - class DateTime < Value # :nodoc: - include Helpers::TimeValue - include Helpers::AcceptsMultiparameterTime.new( - defaults: { 4 => 0, 5 => 0 } - ) - - def type - :datetime - end - - private - - def cast_value(string) - return string unless string.is_a?(::String) - return if string.empty? - - fast_string_to_time(string) || fallback_string_to_time(string) - end - - # '0.123456' -> 123456 - # '1.123456' -> 123456 - def microseconds(time) - time[:sec_fraction] ? (time[:sec_fraction] * 1_000_000).to_i : 0 - end - - def fallback_string_to_time(string) - time_hash = ::Date._parse(string) - time_hash[:sec_fraction] = microseconds(time_hash) - - new_time(*time_hash.values_at(:year, :mon, :mday, :hour, :min, :sec, :sec_fraction, :offset)) - end - - def value_from_multiparameter_assignment(values_hash) - missing_parameter = (1..3).detect { |key| !values_hash.key?(key) } - if missing_parameter - raise ArgumentError, missing_parameter - end - super - end + class DateTime < ActiveModel::Type::DateTime + include Internal::Timezone end end end diff --git a/activerecord/lib/active_record/type/decimal.rb b/activerecord/lib/active_record/type/decimal.rb deleted file mode 100644 index f200a92d10..0000000000 --- a/activerecord/lib/active_record/type/decimal.rb +++ /dev/null @@ -1,48 +0,0 @@ -module ActiveRecord - module Type - class Decimal < Value # :nodoc: - include Helpers::Numeric - - def type - :decimal - end - - def type_cast_for_schema(value) - value.to_s.inspect - end - - private - - def cast_value(value) - case value - when ::Float - convert_float_to_big_decimal(value) - when ::Numeric, ::String - BigDecimal(value, precision.to_i) - else - if value.respond_to?(:to_d) - value.to_d - else - cast_value(value.to_s) - end - end - end - - def convert_float_to_big_decimal(value) - if precision - BigDecimal(value, float_precision) - else - value.to_d - end - end - - def float_precision - if precision.to_i > ::Float::DIG + 1 - ::Float::DIG + 1 - else - precision.to_i - end - end - end - end -end diff --git a/activerecord/lib/active_record/type/decimal_without_scale.rb b/activerecord/lib/active_record/type/decimal_without_scale.rb index ff5559e300..7ce33e9cd3 100644 --- a/activerecord/lib/active_record/type/decimal_without_scale.rb +++ b/activerecord/lib/active_record/type/decimal_without_scale.rb @@ -1,8 +1,6 @@ -require 'active_record/type/big_integer' - module ActiveRecord module Type - class DecimalWithoutScale < BigInteger # :nodoc: + class DecimalWithoutScale < ActiveModel::Type::BigInteger # :nodoc: def type :decimal end diff --git a/activerecord/lib/active_record/type/float.rb b/activerecord/lib/active_record/type/float.rb deleted file mode 100644 index d88482b85d..0000000000 --- a/activerecord/lib/active_record/type/float.rb +++ /dev/null @@ -1,25 +0,0 @@ -module ActiveRecord - module Type - class Float < Value # :nodoc: - include Helpers::Numeric - - def type - :float - end - - alias serialize cast - - private - - def cast_value(value) - case value - when ::Float then value - when "Infinity" then ::Float::INFINITY - when "-Infinity" then -::Float::INFINITY - when "NaN" then ::Float::NAN - else value.to_f - end - end - end - end -end diff --git a/activerecord/lib/active_record/type/hash_lookup_type_map.rb b/activerecord/lib/active_record/type/hash_lookup_type_map.rb index 3b01e3f8ca..0145d5d6c1 100644 --- a/activerecord/lib/active_record/type/hash_lookup_type_map.rb +++ b/activerecord/lib/active_record/type/hash_lookup_type_map.rb @@ -15,9 +15,9 @@ module ActiveRecord private - def perform_fetch(type, *args, &block) - @mapping.fetch(type, block).call(type, *args) - end + def perform_fetch(type, *args, &block) + @mapping.fetch(type, block).call(type, *args) + end end end end diff --git a/activerecord/lib/active_record/type/helpers.rb b/activerecord/lib/active_record/type/helpers.rb deleted file mode 100644 index 634d417d13..0000000000 --- a/activerecord/lib/active_record/type/helpers.rb +++ /dev/null @@ -1,4 +0,0 @@ -require 'active_record/type/helpers/accepts_multiparameter_time' -require 'active_record/type/helpers/numeric' -require 'active_record/type/helpers/mutable' -require 'active_record/type/helpers/time_value' diff --git a/activerecord/lib/active_record/type/helpers/accepts_multiparameter_time.rb b/activerecord/lib/active_record/type/helpers/accepts_multiparameter_time.rb deleted file mode 100644 index be571fc1c7..0000000000 --- a/activerecord/lib/active_record/type/helpers/accepts_multiparameter_time.rb +++ /dev/null @@ -1,30 +0,0 @@ -module ActiveRecord - module Type - module Helpers - class AcceptsMultiparameterTime < Module # :nodoc: - def initialize(defaults: {}) - define_method(:cast) do |value| - if value.is_a?(Hash) - value_from_multiparameter_assignment(value) - else - super(value) - end - end - - define_method(:value_from_multiparameter_assignment) do |values_hash| - defaults.each do |k, v| - values_hash[k] ||= v - end - return unless values_hash[1] && values_hash[2] && values_hash[3] - values = values_hash.sort.map(&:last) - ::Time.send( - ActiveRecord::Base.default_timezone, - *values - ) - end - private :value_from_multiparameter_assignment - end - end - end - end -end diff --git a/activerecord/lib/active_record/type/helpers/mutable.rb b/activerecord/lib/active_record/type/helpers/mutable.rb deleted file mode 100644 index 88a9099277..0000000000 --- a/activerecord/lib/active_record/type/helpers/mutable.rb +++ /dev/null @@ -1,18 +0,0 @@ -module ActiveRecord - module Type - module Helpers - module Mutable # :nodoc: - def cast(value) - deserialize(serialize(value)) - end - - # +raw_old_value+ will be the `_before_type_cast` version of the - # value (likely a string). +new_value+ will be the current, type - # cast value. - def changed_in_place?(raw_old_value, new_value) - raw_old_value != serialize(new_value) - end - end - end - end -end diff --git a/activerecord/lib/active_record/type/helpers/numeric.rb b/activerecord/lib/active_record/type/helpers/numeric.rb deleted file mode 100644 index a755a02a59..0000000000 --- a/activerecord/lib/active_record/type/helpers/numeric.rb +++ /dev/null @@ -1,34 +0,0 @@ -module ActiveRecord - module Type - module Helpers - module Numeric # :nodoc: - def cast(value) - value = case value - when true then 1 - when false then 0 - when ::String then value.presence - else value - end - super(value) - end - - def changed?(old_value, _new_value, new_value_before_type_cast) # :nodoc: - super || number_to_non_number?(old_value, new_value_before_type_cast) - end - - private - - def number_to_non_number?(old_value, new_value_before_type_cast) - old_value != nil && non_numeric_string?(new_value_before_type_cast) - end - - def non_numeric_string?(value) - # 'wibble'.to_i will give zero, we want to make sure - # that we aren't marking int zero to string zero as - # changed. - value.to_s !~ /\A-?\d+\.?\d*\z/ - end - end - end - end -end diff --git a/activerecord/lib/active_record/type/helpers/time_value.rb b/activerecord/lib/active_record/type/helpers/time_value.rb deleted file mode 100644 index 7eb41557cb..0000000000 --- a/activerecord/lib/active_record/type/helpers/time_value.rb +++ /dev/null @@ -1,58 +0,0 @@ -module ActiveRecord - module Type - module Helpers - module TimeValue # :nodoc: - def serialize(value) - if precision && value.respond_to?(:usec) - number_of_insignificant_digits = 6 - precision - round_power = 10 ** number_of_insignificant_digits - value = value.change(usec: value.usec / round_power * round_power) - end - - if value.acts_like?(:time) - zone_conversion_method = ActiveRecord::Base.default_timezone == :utc ? :getutc : :getlocal - - if value.respond_to?(zone_conversion_method) - value = value.send(zone_conversion_method) - end - end - - value - end - - def type_cast_for_schema(value) - "'#{value.to_s(:db)}'" - end - - def user_input_in_time_zone(value) - value.in_time_zone - end - - private - - def new_time(year, mon, mday, hour, min, sec, microsec, offset = nil) - # Treat 0000-00-00 00:00:00 as nil. - return if year.nil? || (year == 0 && mon == 0 && mday == 0) - - if offset - time = ::Time.utc(year, mon, mday, hour, min, sec, microsec) rescue nil - return unless time - - time -= offset - Base.default_timezone == :utc ? time : time.getlocal - else - ::Time.public_send(Base.default_timezone, year, mon, mday, hour, min, sec, microsec) rescue nil - end - end - - # Doesn't handle time zones. - def fast_string_to_time(string) - if string =~ ConnectionAdapters::Column::Format::ISO_DATETIME - microsec = ($7.to_r * 1_000_000).to_i - new_time $1.to_i, $2.to_i, $3.to_i, $4.to_i, $5.to_i, $6.to_i, microsec - end - end - end - end - end -end diff --git a/activerecord/lib/active_record/type/integer.rb b/activerecord/lib/active_record/type/integer.rb deleted file mode 100644 index c5040c6d3b..0000000000 --- a/activerecord/lib/active_record/type/integer.rb +++ /dev/null @@ -1,66 +0,0 @@ -module ActiveRecord - module Type - class Integer < Value # :nodoc: - include Helpers::Numeric - - # Column storage size in bytes. - # 4 bytes means a MySQL int or Postgres integer as opposed to smallint etc. - DEFAULT_LIMIT = 4 - - def initialize(*) - super - @range = min_value...max_value - end - - def type - :integer - end - - def deserialize(value) - return if value.nil? - value.to_i - end - - def serialize(value) - result = cast(value) - if result - ensure_in_range(result) - end - result - end - - protected - - attr_reader :range - - private - - def cast_value(value) - case value - when true then 1 - when false then 0 - else - value.to_i rescue nil - end - end - - def ensure_in_range(value) - unless range.cover?(value) - raise RangeError, "#{value} is out of range for #{self.class} with limit #{_limit}" - end - end - - def max_value - 1 << (_limit * 8 - 1) # 8 bits per byte with one bit for sign - end - - def min_value - -max_value - end - - def _limit - self.limit || DEFAULT_LIMIT - end - end - end -end diff --git a/activerecord/lib/active_record/type/internal/abstract_json.rb b/activerecord/lib/active_record/type/internal/abstract_json.rb new file mode 100644 index 0000000000..e19c5a14da --- /dev/null +++ b/activerecord/lib/active_record/type/internal/abstract_json.rb @@ -0,0 +1,33 @@ +module ActiveRecord + module Type + module Internal # :nodoc: + class AbstractJson < ActiveModel::Type::Value # :nodoc: + include ActiveModel::Type::Helpers::Mutable + + def type + :json + end + + def deserialize(value) + if value.is_a?(::String) + ::ActiveSupport::JSON.decode(value) rescue nil + else + value + end + end + + def serialize(value) + if value.nil? + nil + else + ::ActiveSupport::JSON.encode(value) + end + end + + def accessor + ActiveRecord::Store::StringKeyedHashAccessor + end + end + end + end +end diff --git a/activerecord/lib/active_record/type/internal/timezone.rb b/activerecord/lib/active_record/type/internal/timezone.rb new file mode 100644 index 0000000000..947e06158a --- /dev/null +++ b/activerecord/lib/active_record/type/internal/timezone.rb @@ -0,0 +1,15 @@ +module ActiveRecord + module Type + module Internal + module Timezone + def is_utc? + ActiveRecord::Base.default_timezone == :utc + end + + def default_timezone + ActiveRecord::Base.default_timezone + end + end + end + end +end diff --git a/activerecord/lib/active_record/type/serialized.rb b/activerecord/lib/active_record/type/serialized.rb index ea3e0d6a45..ac9134bfcb 100644 --- a/activerecord/lib/active_record/type/serialized.rb +++ b/activerecord/lib/active_record/type/serialized.rb @@ -1,7 +1,7 @@ module ActiveRecord module Type - class Serialized < DelegateClass(Type::Value) # :nodoc: - include Helpers::Mutable + class Serialized < DelegateClass(ActiveModel::Type::Value) # :nodoc: + include ActiveModel::Type::Helpers::Mutable attr_reader :subtype, :coder @@ -32,7 +32,7 @@ module ActiveRecord def changed_in_place?(raw_old_value, value) return false if value.nil? - raw_new_value = serialize(value) + raw_new_value = encoded(value) raw_old_value.nil? != raw_new_value.nil? || subtype.changed_in_place?(raw_old_value, raw_new_value) end @@ -41,11 +41,23 @@ module ActiveRecord ActiveRecord::Store::IndifferentHashAccessor end + def assert_valid_value(value) + if coder.respond_to?(:assert_valid_value) + coder.assert_valid_value(value) + end + end + private - def default_value?(value) - value == coder.load(nil) - end + def default_value?(value) + value == coder.load(nil) + end + + def encoded(value) + unless default_value?(value) + coder.dump(value) + end + end end end end diff --git a/activerecord/lib/active_record/type/string.rb b/activerecord/lib/active_record/type/string.rb deleted file mode 100644 index 2662b7e874..0000000000 --- a/activerecord/lib/active_record/type/string.rb +++ /dev/null @@ -1,36 +0,0 @@ -module ActiveRecord - module Type - class String < Value # :nodoc: - def type - :string - end - - def changed_in_place?(raw_old_value, new_value) - if new_value.is_a?(::String) - raw_old_value != new_value - end - end - - def serialize(value) - case value - when ::Numeric, ActiveSupport::Duration then value.to_s - when ::String then ::String.new(value) - when true then "t" - when false then "f" - else super - end - end - - private - - def cast_value(value) - case value - when true then "t" - when false then "f" - # String.new is slightly faster than dup - else ::String.new(value.to_s) - end - end - end - end -end diff --git a/activerecord/lib/active_record/type/text.rb b/activerecord/lib/active_record/type/text.rb index 26f980f060..cb1949700a 100644 --- a/activerecord/lib/active_record/type/text.rb +++ b/activerecord/lib/active_record/type/text.rb @@ -1,8 +1,6 @@ -require 'active_record/type/string' - module ActiveRecord module Type - class Text < String # :nodoc: + class Text < ActiveModel::Type::String # :nodoc: def type :text end diff --git a/activerecord/lib/active_record/type/time.rb b/activerecord/lib/active_record/type/time.rb index 19a10021bc..b9bac87c67 100644 --- a/activerecord/lib/active_record/type/time.rb +++ b/activerecord/lib/active_record/type/time.rb @@ -1,40 +1,17 @@ module ActiveRecord module Type - class Time < Value # :nodoc: - include Helpers::TimeValue - include Helpers::AcceptsMultiparameterTime.new( - defaults: { 1 => 1970, 2 => 1, 3 => 1, 4 => 0, 5 => 0 } - ) + class Time < ActiveModel::Type::Time + include Internal::Timezone - def type - :time + class Value < DelegateClass(::Time) # :nodoc: end - def user_input_in_time_zone(value) - return unless value.present? - - case value - when ::String - value = "2000-01-01 #{value}" + def serialize(value) + case value = super when ::Time - value = value.change(year: 2000, day: 1, month: 1) - end - - super(value) - end - - private - - def cast_value(value) - return value unless value.is_a?(::String) - return if value.empty? - - dummy_time_value = "2000-01-01 #{value}" - - fast_string_to_time(dummy_time_value) || begin - time_hash = ::Date._parse(dummy_time_value) - return if time_hash[:hour].nil? - new_time(*time_hash.values_at(:year, :mon, :mday, :hour, :min, :sec, :sec_fraction)) + Value.new(value) + else + value end end end diff --git a/activerecord/lib/active_record/type/type_map.rb b/activerecord/lib/active_record/type/type_map.rb index 09f5ba6b74..7bce82a1ff 100644 --- a/activerecord/lib/active_record/type/type_map.rb +++ b/activerecord/lib/active_record/type/type_map.rb @@ -1,17 +1,17 @@ -require 'thread_safe' +require "concurrent/map" module ActiveRecord module Type class TypeMap # :nodoc: def initialize @mapping = {} - @cache = ThreadSafe::Cache.new do |h, key| - h.fetch_or_store(key, ThreadSafe::Cache.new) + @cache = Concurrent::Map.new do |h, key| + h.fetch_or_store(key, Concurrent::Map.new) end end def lookup(lookup_key, *args) - fetch(lookup_key, *args) { default_value } + fetch(lookup_key, *args) { Type.default_value } end def fetch(lookup_key, *args, &block) @@ -44,21 +44,17 @@ module ActiveRecord private - def perform_fetch(lookup_key, *args) - matching_pair = @mapping.reverse_each.detect do |key, _| - key === lookup_key - end + def perform_fetch(lookup_key, *args) + matching_pair = @mapping.reverse_each.detect do |key, _| + key === lookup_key + end - if matching_pair - matching_pair.last.call(lookup_key, *args) - else - yield lookup_key, *args + if matching_pair + matching_pair.last.call(lookup_key, *args) + else + yield lookup_key, *args + end end - end - - def default_value - @default_value ||= Value.new - end end end end diff --git a/activerecord/lib/active_record/type/unsigned_integer.rb b/activerecord/lib/active_record/type/unsigned_integer.rb index ed3e527483..9ae0109f9f 100644 --- a/activerecord/lib/active_record/type/unsigned_integer.rb +++ b/activerecord/lib/active_record/type/unsigned_integer.rb @@ -1,15 +1,15 @@ module ActiveRecord module Type - class UnsignedInteger < Integer # :nodoc: + class UnsignedInteger < ActiveModel::Type::Integer # :nodoc: private - def max_value - super * 2 - end + def max_value + super * 2 + end - def min_value - 0 - end + def min_value + 0 + end end end end diff --git a/activerecord/lib/active_record/type/value.rb b/activerecord/lib/active_record/type/value.rb deleted file mode 100644 index 6b9d147ecc..0000000000 --- a/activerecord/lib/active_record/type/value.rb +++ /dev/null @@ -1,104 +0,0 @@ -module ActiveRecord - module Type - class Value - attr_reader :precision, :scale, :limit - - def initialize(precision: nil, limit: nil, scale: nil) - @precision = precision - @scale = scale - @limit = limit - end - - def type # :nodoc: - end - - # Converts a value from database input to the appropriate ruby type. The - # return value of this method will be returned from - # ActiveRecord::AttributeMethods::Read#read_attribute. The default - # implementation just calls Value#cast. - # - # +value+ The raw input, as provided from the database. - def deserialize(value) - cast(value) - end - - # Type casts a value from user input (e.g. from a setter). This value may - # be a string from the form builder, or a ruby object passed to a setter. - # There is currently no way to differentiate between which source it came - # from. - # - # The return value of this method will be returned from - # ActiveRecord::AttributeMethods::Read#read_attribute. See also: - # Value#cast_value. - # - # +value+ The raw input, as provided to the attribute setter. - def cast(value) - cast_value(value) unless value.nil? - end - - # Casts a value from the ruby type to a type that the database knows how - # to understand. The returned value from this method should be a - # +String+, +Numeric+, +Date+, +Time+, +Symbol+, +true+, +false+, or - # +nil+. - def serialize(value) - value - end - - # Type casts a value for schema dumping. This method is private, as we are - # hoping to remove it entirely. - def type_cast_for_schema(value) # :nodoc: - value.inspect - end - - # These predicates are not documented, as I need to look further into - # their use, and see if they can be removed entirely. - def binary? # :nodoc: - false - end - - # Determines whether a value has changed for dirty checking. +old_value+ - # and +new_value+ will always be type-cast. Types should not need to - # override this method. - def changed?(old_value, new_value, _new_value_before_type_cast) - old_value != new_value - end - - # Determines whether the mutable value has been modified since it was - # read. Returns +false+ by default. If your type returns an object - # which could be mutated, you should override this method. You will need - # to either: - # - # - pass +new_value+ to Value#serialize and compare it to - # +raw_old_value+ - # - # or - # - # - pass +raw_old_value+ to Value#deserialize and compare it to - # +new_value+ - # - # +raw_old_value+ The original value, before being passed to - # +deserialize+. - # - # +new_value+ The current value, after type casting. - def changed_in_place?(raw_old_value, new_value) - false - end - - def ==(other) - self.class == other.class && - precision == other.precision && - scale == other.scale && - limit == other.limit - end - - private - - # Convenience method for types which do not need separate type casting - # behavior for user and database inputs. Called by Value#cast for - # values except +nil+. - def cast_value(value) # :doc: - value - end - end - end -end diff --git a/activerecord/lib/active_record/type_caster.rb b/activerecord/lib/active_record/type_caster.rb index 63ba10c289..f1686e4913 100644 --- a/activerecord/lib/active_record/type_caster.rb +++ b/activerecord/lib/active_record/type_caster.rb @@ -1,7 +1,7 @@ -require 'active_record/type_caster/map' -require 'active_record/type_caster/connection' +require "active_record/type_caster/map" +require "active_record/type_caster/connection" module ActiveRecord - module TypeCaster + module TypeCaster # :nodoc: end end diff --git a/activerecord/lib/active_record/type_caster/connection.rb b/activerecord/lib/active_record/type_caster/connection.rb index 3878270770..9f7bbe8843 100644 --- a/activerecord/lib/active_record/type_caster/connection.rb +++ b/activerecord/lib/active_record/type_caster/connection.rb @@ -1,6 +1,6 @@ module ActiveRecord module TypeCaster - class Connection + class Connection # :nodoc: def initialize(klass, table_name) @klass = klass @table_name = table_name @@ -12,18 +12,20 @@ module ActiveRecord connection.type_cast_from_column(column, value) end + # TODO Change this to private once we've dropped Ruby 2.2 support. + # Workaround for Ruby 2.2 "private attribute?" warning. protected - attr_reader :table_name - delegate :connection, to: :@klass + attr_reader :table_name + delegate :connection, to: :@klass private - def column_for(attribute_name) - if connection.schema_cache.table_exists?(table_name) - connection.schema_cache.columns_hash(table_name)[attribute_name.to_s] + def column_for(attribute_name) + if connection.schema_cache.data_source_exists?(table_name) + connection.schema_cache.columns_hash(table_name)[attribute_name.to_s] + end end - end end end end diff --git a/activerecord/lib/active_record/type_caster/map.rb b/activerecord/lib/active_record/type_caster/map.rb index 4b1941351c..9f79723125 100644 --- a/activerecord/lib/active_record/type_caster/map.rb +++ b/activerecord/lib/active_record/type_caster/map.rb @@ -1,6 +1,6 @@ module ActiveRecord module TypeCaster - class Map + class Map # :nodoc: def initialize(types) @types = types end @@ -11,9 +11,11 @@ module ActiveRecord type.serialize(value) end + # TODO Change this to private once we've dropped Ruby 2.2 support. + # Workaround for Ruby 2.2 "private attribute?" warning. protected - attr_reader :types + attr_reader :types end end end diff --git a/activerecord/lib/active_record/validations.rb b/activerecord/lib/active_record/validations.rb index 34d96b19fe..9633f226f0 100644 --- a/activerecord/lib/active_record/validations.rb +++ b/activerecord/lib/active_record/validations.rb @@ -1,8 +1,9 @@ module ActiveRecord - # = Active Record RecordInvalid + # = Active Record \RecordInvalid # - # Raised by <tt>save!</tt> and <tt>create!</tt> when the record is invalid. Use the - # +record+ method to retrieve the record which did not validate. + # Raised by {ActiveRecord::Base#save!}[rdoc-ref:Persistence#save!] and + # {ActiveRecord::Base#create!}[rdoc-ref:Persistence::ClassMethods#create!] when the record is invalid. + # Use the #record method to retrieve the record which did not validate. # # begin # complex_operation_that_internally_calls_save! @@ -12,62 +13,72 @@ module ActiveRecord class RecordInvalid < ActiveRecordError attr_reader :record - def initialize(record) - @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")) + def initialize(record = nil) + if record + @record = record + errors = @record.errors.full_messages.join(", ") + message = I18n.t(:"#{@record.class.i18n_scope}.errors.messages.record_invalid", errors: errors, default: :"errors.messages.record_invalid") + else + message = "Record invalid" + end + + super(message) end end - # = Active Record Validations + # = Active Record \Validations # - # Active Record includes the majority of its validations from <tt>ActiveModel::Validations</tt> + # Active Record includes the majority of its validations from ActiveModel::Validations # all of which accept the <tt>:on</tt> argument to define the context where the # validations are active. Active Record will always supply either the context of # <tt>:create</tt> or <tt>:update</tt> dependent on whether the model is a - # <tt>new_record?</tt>. + # {new_record?}[rdoc-ref:Persistence#new_record?]. module Validations extend ActiveSupport::Concern include ActiveModel::Validations # The validation process on save can be skipped by passing <tt>validate: false</tt>. - # The regular Base#save method is replaced with this when the validations - # module is mixed in, which it is by default. - def save(options={}) + # The regular {ActiveRecord::Base#save}[rdoc-ref:Persistence#save] method is replaced + # with this when the validations module is mixed in, which it is by default. + def save(options = {}) perform_validations(options) ? super : false end - # Attempts to save the record just like Base#save but will raise a +RecordInvalid+ - # exception instead of returning +false+ if the record is not valid. - def save!(options={}) + # Attempts to save the record just like {ActiveRecord::Base#save}[rdoc-ref:Base#save] but + # will raise an ActiveRecord::RecordInvalid exception instead of returning +false+ if the record is not valid. + def save!(options = {}) perform_validations(options) ? super : raise_validation_error end # Runs all the validations within the specified context. Returns +true+ if # no errors are found, +false+ otherwise. # - # Aliased as validate. + # Aliased as #validate. # # If the argument is +false+ (default is +nil+), the context is set to <tt>:create</tt> if - # <tt>new_record?</tt> is +true+, and to <tt>:update</tt> if it is not. + # {new_record?}[rdoc-ref:Persistence#new_record?] is +true+, and to <tt>:update</tt> if it is not. # - # Validations with no <tt>:on</tt> option will run no matter the context. Validations with + # \Validations with no <tt>:on</tt> option will run no matter the context. \Validations with # some <tt>:on</tt> option will only run in the specified context. def valid?(context = nil) - context ||= (new_record? ? :create : :update) + context ||= default_validation_context output = super(context) errors.empty? && output end alias_method :validate, :valid? - protected + private + + def default_validation_context + new_record? ? :create : :update + end def raise_validation_error raise(RecordInvalid.new(self)) end - def perform_validations(options={}) # :nodoc: + def perform_validations(options = {}) options[:validate] == false || valid?(options[:context]) end end diff --git a/activerecord/lib/active_record/validations/absence.rb b/activerecord/lib/active_record/validations/absence.rb index 2e19e6dc5c..641d041f3d 100644 --- a/activerecord/lib/active_record/validations/absence.rb +++ b/activerecord/lib/active_record/validations/absence.rb @@ -2,7 +2,6 @@ module ActiveRecord module Validations class AbsenceValidator < ActiveModel::Validations::AbsenceValidator # :nodoc: def validate_each(record, attribute, association_or_value) - return unless should_validate?(record) if record.class._reflect_on_association(attribute) association_or_value = Array.wrap(association_or_value).reject(&:marked_for_destruction?) end diff --git a/activerecord/lib/active_record/validations/associated.rb b/activerecord/lib/active_record/validations/associated.rb index 47ccef31a5..c695965d7b 100644 --- a/activerecord/lib/active_record/validations/associated.rb +++ b/activerecord/lib/active_record/validations/associated.rb @@ -2,10 +2,16 @@ module ActiveRecord module Validations class AssociatedValidator < ActiveModel::EachValidator #:nodoc: def validate_each(record, attribute, value) - if Array.wrap(value).reject {|r| r.marked_for_destruction? || r.valid?}.any? - record.errors.add(attribute, :invalid, options.merge(:value => value)) + if Array(value).reject { |r| valid_object?(r) }.any? + record.errors.add(attribute, :invalid, options.merge(value: value)) end end + + private + + def valid_object?(record) + (record.respond_to?(:marked_for_destruction?) && record.marked_for_destruction?) || record.valid? + end end module ClassMethods @@ -24,13 +30,14 @@ module ActiveRecord # # NOTE: This validation will not fail if the association hasn't been # assigned. If you want to ensure that the association is both present and - # guaranteed to be valid, you also need to use +validates_presence_of+. + # guaranteed to be valid, you also need to use + # {validates_presence_of}[rdoc-ref:Validations::ClassMethods#validates_presence_of]. # # Configuration options: # # * <tt>:message</tt> - A custom error message (default is: "is invalid"). # * <tt>:on</tt> - Specifies the contexts where this validation is active. - # Runs in all validation contexts by default (nil). You can pass a symbol + # Runs in all validation contexts by default +nil+. You can pass a symbol # or an array of symbols. (e.g. <tt>on: :create</tt> or # <tt>on: :custom_validation_context</tt> or # <tt>on: [:create, :custom_validation_context]</tt>) diff --git a/activerecord/lib/active_record/validations/length.rb b/activerecord/lib/active_record/validations/length.rb index 69e048eef1..0e0cebce4a 100644 --- a/activerecord/lib/active_record/validations/length.rb +++ b/activerecord/lib/active_record/validations/length.rb @@ -2,23 +2,11 @@ module ActiveRecord module Validations class LengthValidator < ActiveModel::Validations::LengthValidator # :nodoc: def validate_each(record, attribute, association_or_value) - return unless should_validate?(record) || associations_are_dirty?(record) if association_or_value.respond_to?(:loaded?) && association_or_value.loaded? association_or_value = association_or_value.target.reject(&:marked_for_destruction?) end super end - - def associations_are_dirty?(record) - attributes.any? do |attribute| - value = record.read_attribute_for_validation(attribute) - if value.respond_to?(:loaded?) && value.loaded? - value.target.any?(&:marked_for_destruction?) - else - false - end - end - end end module ClassMethods diff --git a/activerecord/lib/active_record/validations/presence.rb b/activerecord/lib/active_record/validations/presence.rb index 23a3985d35..ca5eda2f84 100644 --- a/activerecord/lib/active_record/validations/presence.rb +++ b/activerecord/lib/active_record/validations/presence.rb @@ -2,7 +2,6 @@ module ActiveRecord module Validations class PresenceValidator < ActiveModel::Validations::PresenceValidator # :nodoc: def validate_each(record, attribute, association_or_value) - return unless should_validate?(record) if record.class._reflect_on_association(attribute) association_or_value = Array.wrap(association_or_value).reject(&:marked_for_destruction?) end @@ -31,7 +30,7 @@ module ActiveRecord # This is due to the way Object#blank? handles boolean values: # <tt>false.blank? # => true</tt>. # - # This validator defers to the ActiveModel validation for presence, adding the + # This validator defers to the Active Model validation for presence, adding the # check to see that an associated object is not marked for destruction. This # prevents the parent object from validating successfully and saving, which then # deletes the associated object, thus putting the parent object into an invalid @@ -39,12 +38,13 @@ module ActiveRecord # # NOTE: This validation will not fail while using it with an association # if the latter was assigned but not valid. If you want to ensure that - # it is both present and valid, you also need to use +validates_associated+. + # it is both present and valid, you also need to use + # {validates_associated}[rdoc-ref:Validations::ClassMethods#validates_associated]. # # Configuration options: # * <tt>:message</tt> - A custom error message (default is: "can't be blank"). # * <tt>:on</tt> - Specifies the contexts where this validation is active. - # Runs in all validation contexts by default (nil). You can pass a symbol + # Runs in all validation contexts by default +nil+. You can pass a symbol # or an array of symbols. (e.g. <tt>on: :create</tt> or # <tt>on: :custom_validation_context</tt> or # <tt>on: [:create, :custom_validation_context]</tt>) @@ -57,7 +57,7 @@ module ActiveRecord # or <tt>unless: Proc.new { |user| user.signup_step <= 2 }</tt>). The method, # proc or string should return or evaluate to a +true+ or +false+ value. # * <tt>:strict</tt> - Specifies whether validation should be strict. - # See <tt>ActiveModel::Validation#validates!</tt> for more information. + # See ActiveModel::Validation#validates! for more information. def validates_presence_of(*attr_names) validates_with PresenceValidator, _merge_attributes(attr_names) end diff --git a/activerecord/lib/active_record/validations/uniqueness.rb b/activerecord/lib/active_record/validations/uniqueness.rb index 5106f4e127..9e8edfbfaf 100644 --- a/activerecord/lib/active_record/validations/uniqueness.rb +++ b/activerecord/lib/active_record/validations/uniqueness.rb @@ -11,14 +11,18 @@ module ActiveRecord end def validate_each(record, attribute, value) - return unless should_validate?(record) finder_class = find_finder_class_for(record) - table = finder_class.arel_table value = map_enum_attribute(finder_class, attribute, value) - relation = build_relation(finder_class, table, attribute, value) - relation = relation.where.not(finder_class.primary_key => record.id) if record.persisted? - relation = scope_relation(record, table, relation) + relation = build_relation(finder_class, attribute, value) + if record.persisted? + if finder_class.primary_key + relation = relation.where.not(finder_class.primary_key => record.id_in_database || record.id) + else + raise UnknownPrimaryKey.new(finder_class, "Can not validate uniqueness for persisted record without primary key.") + end + end + relation = scope_relation(record, relation) relation = relation.merge(options[:conditions]) if options[:conditions] if relation.exists? @@ -29,13 +33,13 @@ module ActiveRecord end end - protected + private # The check for an existing value should be run from a class that # isn't abstract. This means working down from the current class # (self), to the first non-abstract class. Since classes don't know # their subclasses, we have to build the hierarchy between self and # the record's class. - def find_finder_class_for(record) #:nodoc: + def find_finder_class_for(record) class_hierarchy = [record.class] while class_hierarchy.first != @klass @@ -45,48 +49,46 @@ module ActiveRecord class_hierarchy.detect { |klass| !klass.abstract_class? } end - def build_relation(klass, table, attribute, value) #:nodoc: + def build_relation(klass, attribute, value) if reflection = klass._reflect_on_association(attribute) attribute = reflection.foreign_key value = value.attributes[reflection.klass.primary_key] unless value.nil? end - attribute_name = attribute.to_s + if value.nil? + return klass.unscoped.where!(attribute => value) + end # the attribute may be an aliased attribute - if klass.attribute_aliases[attribute_name] - attribute = klass.attribute_aliases[attribute_name] - attribute_name = attribute.to_s + if klass.attribute_alias?(attribute) + attribute = klass.attribute_alias(attribute) end + attribute_name = attribute.to_s + + table = klass.arel_table column = klass.columns_hash[attribute_name] cast_type = klass.type_for_attribute(attribute_name) - value = cast_type.serialize(value) - value = klass.connection.type_cast(value) - if value.is_a?(String) && column.limit - value = value.to_s[0, column.limit] - end - value = Arel::Nodes::Quoted.new(value) - - comparison = if !options[:case_sensitive] && !value.nil? + comparison = if !options[:case_sensitive] # will use SQL LOWER function before comparison, unless it detects a case insensitive collation klass.connection.case_insensitive_comparison(table, attribute, column, value) else klass.connection.case_sensitive_comparison(table, attribute, column, value) end - klass.unscoped.where(comparison) - rescue RangeError - klass.none + klass.unscoped.tap do |scope| + parts = [comparison] + binds = [Relation::QueryAttribute.new(attribute_name, value, cast_type)] + scope.where_clause += Relation::WhereClause.new(parts, binds) + end end - def scope_relation(record, table, relation) + def scope_relation(record, relation) Array(options[:scope]).each do |scope_item| - if reflection = record.class._reflect_on_association(scope_item) - scope_value = record.send(reflection.foreign_key) - scope_item = reflection.foreign_key + scope_value = if record.class._reflect_on_association(scope_item) + record.association(scope_item).reader else - scope_value = record._read_attribute(scope_item) + record._read_attribute(scope_item) end relation = relation.where(scope_item => scope_value) end @@ -166,7 +168,8 @@ module ActiveRecord # # === Concurrency and integrity # - # Using this validation method in conjunction with ActiveRecord::Base#save + # Using this validation method in conjunction with + # {ActiveRecord::Base#save}[rdoc-ref:Persistence#save] # does not guarantee the absence of duplicate record insertions, because # uniqueness checks on the application level are inherently prone to race # conditions. For example, suppose that two users try to post a Comment at @@ -203,12 +206,12 @@ module ActiveRecord # This could even happen if you use transactions with the 'serializable' # isolation level. The best way to work around this problem is to add a unique # index to the database table using - # ActiveRecord::ConnectionAdapters::SchemaStatements#add_index. In the - # rare case that a race condition occurs, the database will guarantee + # {connection.add_index}[rdoc-ref:ConnectionAdapters::SchemaStatements#add_index]. + # In the rare case that a race condition occurs, the database will guarantee # the field's uniqueness. # # When the database catches such a duplicate insertion, - # ActiveRecord::Base#save will raise an ActiveRecord::StatementInvalid + # {ActiveRecord::Base#save}[rdoc-ref:Persistence#save] will raise an ActiveRecord::StatementInvalid # exception. You can either choose to let this error propagate (which # will result in the default Rails exception page being shown), or you # can catch it and restart the transaction (e.g. by telling the user @@ -224,7 +227,6 @@ module ActiveRecord # # The following bundled adapters throw the ActiveRecord::RecordNotUnique exception: # - # * ActiveRecord::ConnectionAdapters::MysqlAdapter. # * ActiveRecord::ConnectionAdapters::Mysql2Adapter. # * ActiveRecord::ConnectionAdapters::SQLite3Adapter. # * ActiveRecord::ConnectionAdapters::PostgreSQLAdapter. diff --git a/activerecord/lib/active_record/version.rb b/activerecord/lib/active_record/version.rb index cf76a13b44..146cfacc18 100644 --- a/activerecord/lib/active_record/version.rb +++ b/activerecord/lib/active_record/version.rb @@ -1,4 +1,4 @@ -require_relative 'gem_version' +require_relative "gem_version" module ActiveRecord # Returns the version of the currently loaded ActiveRecord as a <tt>Gem::Version</tt> |