Upgrade to Rails 2.2.0

As a side benefit, fix an (non-user-visible) bug in display_s5().
Also fixed a bug where removing orphaned pages did not expire cached summary pages.
This commit is contained in:
Jacques Distler 2008-10-27 01:47:01 -05:00
parent 39348c65c2
commit 7600aef48b
827 changed files with 123652 additions and 11027 deletions

View file

@ -21,17 +21,14 @@
# WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
#++
$:.unshift(File.dirname(__FILE__)) unless
$:.include?(File.dirname(__FILE__)) || $:.include?(File.expand_path(File.dirname(__FILE__)))
active_support_path = File.dirname(__FILE__) + "/../../activesupport/lib"
if File.exist?(active_support_path)
$:.unshift active_support_path
require 'active_support'
else
require 'rubygems'
gem 'activesupport'
begin
require 'active_support'
rescue LoadError
activesupport_path = "#{File.dirname(__FILE__)}/../../activesupport/lib"
if File.directory?(activesupport_path)
$:.unshift activesupport_path
require 'active_support'
end
end
require 'active_record/base'
@ -54,6 +51,7 @@ require 'active_record/calculations'
require 'active_record/serialization'
require 'active_record/attribute_methods'
require 'active_record/dirty'
require 'active_record/dynamic_finder_match'
ActiveRecord::Base.class_eval do
extend ActiveRecord::QueryCache
@ -78,3 +76,6 @@ end
require 'active_record/connection_adapters/abstract_adapter'
require 'active_record/schema_dumper'
require 'active_record/i18n_interpolation_deprecation'
I18n.load_path << File.dirname(__FILE__) + '/active_record/locale/en-US.yml'

View file

@ -10,10 +10,10 @@ module ActiveRecord
end unless self.new_record?
end
# Active Record implements aggregation through a macro-like class method called +composed_of+ for representing attributes
# 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)
# 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). Example:
#
# class Customer < ActiveRecord::Base
@ -30,10 +30,10 @@ module ActiveRecord
# class Money
# include Comparable
# attr_reader :amount, :currency
# EXCHANGE_RATES = { "USD_TO_DKK" => 6 }
#
# def initialize(amount, currency = "USD")
# @amount, @currency = amount, currency
# EXCHANGE_RATES = { "USD_TO_DKK" => 6 }
#
# def initialize(amount, currency = "USD")
# @amount, @currency = amount, currency
# end
#
# def exchange_to(other_currency)
@ -56,26 +56,26 @@ module ActiveRecord
#
# class Address
# attr_reader :street, :city
# def initialize(street, city)
# @street, @city = street, city
# def initialize(street, city)
# @street, @city = street, city
# end
#
# def close_to?(other_address)
# city == other_address.city
# 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 any other attribute, though:
#
# customer.balance = Money.new(20) # sets the Money value object and the attribute
# customer.balance # => Money value object
# customer.balance.exchanged_to("DKK") # => Money.new(120, "DKK")
# 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
@ -87,8 +87,8 @@ module ActiveRecord
# 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"
# customer.address_street # => "May Street"
# customer.address_city # => "Chicago"
#
# == Writing value objects
#
@ -99,16 +99,55 @@ module ActiveRecord
# relational unique identifiers (such as primary keys). Normal ActiveRecord::Base classes are entity objects.
#
# It's also important to treat the value objects as immutable. Don't allow the Money object to have its amount changed after
# creation. Create a new Money object with the new value instead. This is exemplified by the Money#exchanged_to method that
# creation. Create a new Money object with the new value instead. This is exemplified by the Money#exchange_to method that
# returns a new value object instead of changing its own values. Active Record won't persist value objects that have been
# changed through means other than the writer method.
#
# The immutable requirement is enforced by Active Record by freezing any object assigned as a value object. Attempting to
# The immutable requirement is enforced by Active Record by freezing any object assigned as a value object. Attempting to
# change it afterwards will result in a ActiveSupport::FrozenObjectError.
#
#
# Read more about value objects on http://c2.com/cgi/wiki?ValueObject and on the dangers of not 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://netaddr.rubyforge.org). The constructor for the value class is called +create+ and it
# expects a CIDR address string as a parameter. New values can be assigned to the value object using either another
# NetAddr::CIDR object, a string or an array. The <tt>:constructor</tt> and <tt>:converter</tt> options can be used to
# meet these requirements:
#
# class NetworkResource < ActiveRecord::Base
# 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
@ -122,47 +161,71 @@ module ActiveRecord
# <tt>composed_of :address</tt> adds <tt>address</tt> and <tt>address=(new_address)</tt> methods.
#
# Options are:
# * <tt>:class_name</tt> - specify the class name of the association. Use it only if that name can't be inferred
# * <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 a number of mapping arrays (attribute, parameter) that bind an attribute name
# to a constructor parameter on the value class.
# * <tt>:allow_nil</tt> - specifies that the aggregate object will not be instantiated when all mapped
# attributes are +nil+. Setting the aggregate class to +nil+ has the effect of writing +nil+ to all mapped attributes.
# * <tt>:mapping</tt> - Specifies the mapping of entity attributes to attributes of the value object. Each mapping
# is represented as an array where the first item is the name of the entity attribute and the second item is the
# name the attribute in the value object. The order in which mappings are defined determine the order in which
# attributes are sent to the value class constructor.
# * <tt>:allow_nil</tt> - Specifies that the value object will not be instantiated when all mapped
# attributes are +nil+. Setting the value object to +nil+ has the effect of writing +nil+ to all mapped attributes.
# This defaults to +false+.
#
# An optional block can be passed to convert the argument that is passed to the writer method into an instance of
# <tt>:class_name</tt>. The block will only be called if the argument is not already an instance of <tt>:class_name</tt>.
# * <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>.
#
# Option examples:
# composed_of :temperature, :mapping => %w(reading celsius)
# composed_of(:balance, :class_name => "Money", :mapping => %w(balance amount)) {|balance| balance.to_money }
# 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 = {}, &block)
options.assert_valid_keys(:class_name, :mapping, :allow_nil)
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 ]
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
allow_nil = options[:allow_nil] || false
constructor = options[:constructor] || :new
converter = options[:converter] || block
ActiveSupport::Deprecation.warn('The conversion block has been deprecated, use the :converter option instead.', caller) if block_given?
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)
writer_method(name, class_name, mapping, allow_nil, block)
create_reflection(:composed_of, part_id, options, self)
end
private
def reader_method(name, class_name, mapping, allow_nil)
def reader_method(name, class_name, mapping, allow_nil, constructor)
module_eval do
define_method(name) do |*args|
force_reload = args.first || false
if (instance_variable_get("@#{name}").nil? || force_reload) && (!allow_nil || mapping.any? {|pair| !read_attribute(pair.first).nil? })
instance_variable_set("@#{name}", class_name.constantize.new(*mapping.collect {|pair| read_attribute(pair.first)}))
attrs = mapping.collect {|pair| read_attribute(pair.first)}
object = case constructor
when Symbol
class_name.constantize.send(constructor, *attrs)
when Proc, Method
constructor.call(*attrs)
else
raise ArgumentError, 'Constructor must be a symbol denoting the constructor method to call or a Proc to be invoked.'
end
instance_variable_set("@#{name}", object)
end
instance_variable_get("@#{name}")
end
@ -170,14 +233,23 @@ module ActiveRecord
end
def writer_method(name, class_name, mapping, allow_nil, conversion)
def writer_method(name, class_name, mapping, allow_nil, converter)
module_eval do
define_method("#{name}=") do |part|
if part.nil? && allow_nil
mapping.each { |pair| self[pair.first] = nil }
instance_variable_set("@#{name}", nil)
else
part = conversion.call(part) unless part.is_a?(class_name.constantize) || conversion.nil?
unless part.is_a?(class_name.constantize) || converter.nil?
part = case converter
when Symbol
class_name.constantize.send(converter, part)
when Proc, Method
converter.call(part)
else
raise ArgumentError, 'Converter must be a symbol denoting the converter method to call or a Proc to be invoked.'
end
end
mapping.each { |pair| self[pair.first] = part.send(pair.last) }
instance_variable_set("@#{name}", part.freeze)
end

View file

@ -1,14 +1,88 @@
module ActiveRecord
# See ActiveRecord::AssociationPreload::ClassMethods for documentation.
module AssociationPreload #:nodoc:
def self.included(base)
base.extend(ClassMethods)
end
# Implements the details of eager loading of ActiveRecord associations.
# Application developers should not use this module directly.
#
# ActiveRecord::Base is extended with this module. The source code in
# ActiveRecord::Base references methods defined in this module.
#
# Note that 'eager loading' and 'preloading' are actually the same thing.
# However, there are two different eager loading strategies.
#
# The first one is by using table joins. This was only strategy available
# prior to Rails 2.1. Suppose that you have an Author model with columns
# 'name' and 'age', and a Book model with columns 'name' and 'sales'. Using
# this strategy, ActiveRecord would try to retrieve all data for an author
# and all of its books via a single query:
#
# SELECT * FROM authors
# LEFT OUTER JOIN books ON authors.id = books.id
# WHERE authors.name = 'Ken Akamatsu'
#
# However, this could result in many rows that contain redundant data. After
# having received the first row, we already have enough data to instantiate
# the Author object. In all subsequent rows, only the data for the joined
# 'books' table is useful; the joined 'authors' data is just redundant, and
# processing this redundant data takes memory and CPU time. The problem
# quickly becomes worse and worse as the level of eager loading increases
# (i.e. if ActiveRecord is to eager load the associations' assocations as
# well).
#
# The second strategy is to use multiple database queries, one for each
# level of association. Since Rails 2.1, this is the default strategy. In
# situations where a table join is necessary (e.g. when the +:conditions+
# option references an association's column), it will fallback to the table
# join strategy.
#
# See also ActiveRecord::Associations::ClassMethods, which explains eager
# loading in a more high-level (application developer-friendly) manner.
module ClassMethods
# Loads the named associations for the activerecord record (or records) given
# preload_options is passed only one level deep: don't pass to the child associations when associations is a Hash
protected
# Eager loads the named associations for the given ActiveRecord record(s).
#
# In this description, 'association name' shall refer to the name passed
# to an association creation method. For example, a model that specifies
# <tt>belongs_to :author</tt>, <tt>has_many :buyers</tt> has association
# names +:author+ and +:buyers+.
#
# == Parameters
# +records+ is an array of ActiveRecord::Base. This array needs not be flat,
# i.e. +records+ itself may also contain arrays of records. In any case,
# +preload_associations+ will preload the associations all records by
# flattening +records+.
#
# +associations+ specifies one or more associations that you want to
# preload. It may be:
# - a Symbol or a String which specifies a single association name. For
# example, specifiying +:books+ allows this method to preload all books
# for an Author.
# - an Array which specifies multiple association names. This array
# is processed recursively. For example, specifying <tt>[:avatar, :books]</tt>
# allows this method to preload an author's avatar as well as all of his
# books.
# - a Hash which specifies multiple association names, as well as
# association names for the to-be-preloaded association objects. For
# example, specifying <tt>{ :author => :avatar }</tt> will preload a
# book's author, as well as that author's avatar.
#
# +:associations+ has the same format as the +:include+ option for
# <tt>ActiveRecord::Base.find</tt>. So +associations+ could look like this:
#
# :books
# [ :books, :author ]
# { :author => :avatar }
# [ :books, { :author => :avatar } ]
#
# +preload_options+ contains options that will be passed to ActiveRecord#find
# (which is called under the hood for preloading records). But it is passed
# only one level deep in the +associations+ argument, i.e. it's not passed
# to the child associations when +associations+ is a Hash.
def preload_associations(records, associations, preload_options={})
records = [records].flatten.compact.uniq
return if records.empty?
@ -30,13 +104,19 @@ module ActiveRecord
private
# Preloads a specific named association for the given records. This is
# called by +preload_associations+ as its base case.
def preload_one_association(records, association, preload_options={})
class_to_reflection = {}
# Not all records have the same class, so group then preload
# group on the reflection itself so that if various subclass share the same association then we do not split them
# unncessarily
# unnecessarily
records.group_by {|record| class_to_reflection[record.class] ||= record.class.reflections[association]}.each do |reflection, records|
raise ConfigurationError, "Association named '#{ association }' was not found; perhaps you misspelled it?" unless reflection
# 'reflection.macro' can return 'belongs_to', 'has_many', etc. Thus,
# the following could call 'preload_belongs_to_association',
# 'preload_has_many_association', etc.
send("preload_#{reflection.macro}_association", records, reflection, preload_options)
end
end
@ -77,12 +157,17 @@ module ActiveRecord
end
end
def construct_id_map(records)
# Given a collection of ActiveRecord objects, constructs a Hash which maps
# the objects' IDs to the relevant objects. Returns a 2-tuple
# <tt>(id_to_record_map, ids)</tt> where +id_to_record_map+ is the Hash,
# and +ids+ is an Array of record IDs.
def construct_id_map(records, primary_key=nil)
id_to_record_map = {}
ids = []
records.each do |record|
ids << record.id
mapped_records = (id_to_record_map[record.id.to_s] ||= [])
primary_key ||= record.class.primary_key
ids << record[primary_key]
mapped_records = (id_to_record_map[ids.last.to_s] ||= [])
mapped_records << record
end
ids.uniq!
@ -95,8 +180,8 @@ module ActiveRecord
records.each {|record| record.send(reflection.name).loaded}
options = reflection.options
conditions = "t0.#{reflection.primary_key_name} IN (?)"
conditions << append_conditions(options, preload_options)
conditions = "t0.#{reflection.primary_key_name} #{in_or_equals_for_ids(ids)}"
conditions << append_conditions(reflection, preload_options)
associated_records = reflection.klass.find(:all, :conditions => [conditions, ids],
:include => options[:include],
@ -108,6 +193,7 @@ module ActiveRecord
end
def preload_has_one_association(records, reflection, preload_options={})
return if records.first.send("loaded_#{reflection.name}?")
id_to_record_map, ids = construct_id_map(records)
options = reflection.options
records.each {|record| record.send("set_#{reflection.name}_target", nil)}
@ -129,23 +215,25 @@ module ActiveRecord
end
def preload_has_many_association(records, reflection, preload_options={})
id_to_record_map, ids = construct_id_map(records)
records.each {|record| record.send(reflection.name).loaded}
return if records.first.send(reflection.name).loaded?
options = reflection.options
primary_key_name = reflection.through_reflection_primary_key_name
id_to_record_map, ids = construct_id_map(records, primary_key_name)
records.each {|record| record.send(reflection.name).loaded}
if options[:through]
through_records = preload_through_records(records, reflection, options[:through])
through_reflection = reflections[options[:through]]
through_primary_key = through_reflection.primary_key_name
unless through_records.empty?
source = reflection.source_reflection.name
#add conditions from reflection!
through_records.first.class.preload_associations(through_records, source, reflection.options)
through_records.first.class.preload_associations(through_records, source, options)
through_records.each do |through_record|
add_preloaded_records_to_collection(id_to_record_map[through_record[through_primary_key].to_s],
reflection.name, through_record.send(source))
through_record_id = through_record[reflection.through_reflection_primary_key].to_s
add_preloaded_records_to_collection(id_to_record_map[through_record_id], reflection.name, through_record.send(source))
end
end
else
set_association_collection_records(id_to_record_map, reflection.name, find_associated_records(ids, reflection, preload_options),
reflection.primary_key_name)
@ -185,6 +273,7 @@ module ActiveRecord
end
def preload_belongs_to_association(records, reflection, preload_options={})
return if records.first.send("loaded_#{reflection.name}?")
options = reflection.options
primary_key_name = reflection.primary_key_name
@ -222,8 +311,6 @@ module ActiveRecord
table_name = klass.quoted_table_name
primary_key = klass.primary_key
conditions = "#{table_name}.#{connection.quote_column_name(primary_key)} IN (?)"
conditions << append_conditions(options, preload_options)
column_type = klass.columns.detect{|c| c.name == primary_key}.type
ids = id_map.keys.uniq.map do |id|
if column_type == :integer
@ -234,6 +321,8 @@ module ActiveRecord
id
end
end
conditions = "#{table_name}.#{connection.quote_column_name(primary_key)} #{in_or_equals_for_ids(ids)}"
conditions << append_conditions(reflection, preload_options)
associated_records = klass.find(:all, :conditions => [conditions, ids],
:include => options[:include],
:select => options[:select],
@ -248,13 +337,13 @@ module ActiveRecord
table_name = reflection.klass.quoted_table_name
if interface = reflection.options[:as]
conditions = "#{reflection.klass.quoted_table_name}.#{connection.quote_column_name "#{interface}_id"} IN (?) and #{reflection.klass.quoted_table_name}.#{connection.quote_column_name "#{interface}_type"} = '#{self.base_class.sti_name}'"
conditions = "#{reflection.klass.quoted_table_name}.#{connection.quote_column_name "#{interface}_id"} #{in_or_equals_for_ids(ids)} and #{reflection.klass.quoted_table_name}.#{connection.quote_column_name "#{interface}_type"} = '#{self.base_class.sti_name}'"
else
foreign_key = reflection.primary_key_name
conditions = "#{reflection.klass.quoted_table_name}.#{foreign_key} IN (?)"
conditions = "#{reflection.klass.quoted_table_name}.#{foreign_key} #{in_or_equals_for_ids(ids)}"
end
conditions << append_conditions(options, preload_options)
conditions << append_conditions(reflection, preload_options)
reflection.klass.find(:all,
:select => (preload_options[:select] || options[:select] || "#{table_name}.*"),
@ -270,13 +359,16 @@ module ActiveRecord
instance_eval("%@#{sql.gsub('@', '\@')}@")
end
def append_conditions(options, preload_options)
def append_conditions(reflection, preload_options)
sql = ""
sql << " AND (#{interpolate_sql_for_preload(sanitize_sql(options[:conditions]))})" if options[:conditions]
sql << " AND (#{interpolate_sql_for_preload(reflection.sanitized_conditions)})" if reflection.sanitized_conditions
sql << " AND (#{sanitize_sql preload_options[:conditions]})" if preload_options[:conditions]
sql
end
def in_or_equals_for_ids(ids)
ids.size > 1 ? "IN (?)" : "= ?"
end
end
end
end

File diff suppressed because it is too large Load diff

View file

@ -2,6 +2,19 @@ require 'set'
module ActiveRecord
module Associations
# AssociationCollection is an abstract class that provides common stuff to
# ease the implementation of association proxies that represent
# collections. See the class hierarchy in AssociationProxy.
#
# You need to be careful with assumptions regarding the target: The proxy
# does not fetch records from the database until it needs them, but new
# ones created with +build+ are added to the target. So, the target may be
# non-empty and still lack children waiting to be read from the database.
# If you look directly to the database you cannot assume that's the entire
# collection because new records may have beed added to the target, etc.
#
# If you need to work on all current children, new and existing records,
# +load_target+ and the +loaded+ flag are your friends.
class AssociationCollection < AssociationProxy #:nodoc:
def initialize(owner, reflection)
super
@ -14,7 +27,7 @@ module ActiveRecord
# If using a custom finder_sql, scan the entire collection.
if @reflection.options[:finder_sql]
expects_array = args.first.kind_of?(Array)
ids = args.flatten.compact.uniq.map(&:to_i)
ids = args.flatten.compact.uniq.map { |arg| arg.to_i }
if ids.size == 1
id = ids.first
@ -50,7 +63,7 @@ module ActiveRecord
# Fetches the first one using SQL if possible.
def first(*args)
if fetch_first_or_last_using_find? args
if fetch_first_or_last_using_find?(args)
find(:first, *args)
else
load_target unless loaded?
@ -60,7 +73,7 @@ module ActiveRecord
# Fetches the last one using SQL if possible.
def last(*args)
if fetch_first_or_last_using_find? args
if fetch_first_or_last_using_find?(args)
find(:last, *args)
else
load_target unless loaded?
@ -95,7 +108,7 @@ module ActiveRecord
result = true
load_target if @owner.new_record?
@owner.transaction do
transaction do
flatten_deeper(records).each do |record|
raise_on_type_mismatch(record)
add_record_to_target_with_callbacks(record) do |r|
@ -110,6 +123,21 @@ module ActiveRecord
alias_method :push, :<<
alias_method :concat, :<<
# Starts a transaction in the association class's database connection.
#
# class Author < ActiveRecord::Base
# has_many :books
# end
#
# Author.find(:first).books.transaction do
# # same effect as calling Book.transaction
# end
def transaction(*args)
@reflection.klass.transaction(*args) do
yield
end
end
# Remove all records from this association
def delete_all
load_target
@ -126,12 +154,47 @@ module ActiveRecord
end
end
# Remove +records+ from this association. Does not destroy +records+.
# Count all records using SQL. If the +:counter_sql+ option is set for the association, it will
# be used for the query. If no +:counter_sql+ was supplied, but +:finder_sql+ was set, the
# descendant's +construct_sql+ method will have set :counter_sql automatically.
# Otherwise, construct options and pass them with scope to the target class's +count+.
def count(*args)
if @reflection.options[:counter_sql]
@reflection.klass.count_by_sql(@counter_sql)
else
column_name, options = @reflection.klass.send(:construct_count_options_from_args, *args)
if @reflection.options[:uniq]
# This is needed because 'SELECT count(DISTINCT *)..' is not valid SQL.
column_name = "#{@reflection.quoted_table_name}.#{@reflection.klass.primary_key}" if column_name == :all
options.merge!(:distinct => true)
end
value = @reflection.klass.send(:with_scope, construct_scope) { @reflection.klass.count(column_name, options) }
limit = @reflection.options[:limit]
offset = @reflection.options[:offset]
if limit || offset
[ [value - offset.to_i, 0].max, limit.to_i ].min
else
value
end
end
end
# Removes +records+ from this association calling +before_remove+ and
# +after_remove+ callbacks.
#
# This method is abstract in the sense that +delete_records+ has to be
# provided by descendants. Note this method does not imply the records
# are actually removed from the database, that depends precisely on
# +delete_records+. They are in any case removed from the collection.
def delete(*records)
records = flatten_deeper(records)
records.each { |record| raise_on_type_mismatch(record) }
@owner.transaction do
transaction do
records.each { |record| callback(:before_remove, record) }
old_records = records.reject {|r| r.new_record? }
@ -158,7 +221,7 @@ module ActiveRecord
end
def destroy_all
@owner.transaction do
transaction do
each { |record| record.destroy }
end
@ -183,12 +246,21 @@ module ActiveRecord
end
end
# Returns the size of the collection by executing a SELECT COUNT(*) query if the collection hasn't been loaded and
# calling collection.size if it has. If it's more likely than not that the collection does have a size larger than zero
# and you need to fetch that collection afterwards, it'll take one less SELECT query if you use length.
# Returns the size of the collection by executing a SELECT COUNT(*)
# query if the collection hasn't been loaded, and calling
# <tt>collection.size</tt> if it has.
#
# If the collection has been already loaded +size+ and +length+ are
# equivalent. If not and you are going to need the records anyway
# +length+ will take one less query. Otherwise +size+ is more efficient.
#
# This method is abstract in the sense that it relies on
# +count_records+, which is a method descendants have to provide.
def size
if @owner.new_record? || (loaded? && !@reflection.options[:uniq])
@target.size
elsif !loaded? && @reflection.options[:group]
load_target.size
elsif !loaded? && !@reflection.options[:uniq] && @target.is_a?(Array)
unsaved_records = @target.select { |r| r.new_record? }
unsaved_records.size + count_records
@ -197,12 +269,18 @@ module ActiveRecord
end
end
# Returns the size of the collection by loading it and calling size on the array. If you want to use this method to check
# whether the collection is empty, use collection.length.zero? instead of collection.empty?
# Returns the size of the collection calling +size+ on the target.
#
# If the collection has been already loaded +length+ and +size+ are
# equivalent. If not and you are going to need the records anyway this
# method will take one less query. Otherwise +size+ is more efficient.
def length
load_target.size
end
# Equivalent to <tt>collection.size.zero?</tt>. If the collection has
# not been already loaded and you are going to fetch the records anyway
# it is better to check <tt>collection.length.zero?</tt>.
def empty?
size.zero?
end
@ -235,7 +313,7 @@ module ActiveRecord
other = other_array.size < 100 ? other_array : other_array.to_set
current = @target.size < 100 ? @target : @target.to_set
@owner.transaction do
transaction do
delete(@target.select { |v| !other.include?(v) })
concat(other_array.select { |v| !current.include?(v) })
end
@ -248,6 +326,10 @@ module ActiveRecord
exists?(record)
end
def proxy_respond_to?(method, include_private = false)
super || @reflection.klass.respond_to?(method, include_private)
end
protected
def construct_find_options!(options)
end
@ -316,7 +398,9 @@ module ActiveRecord
def create_record(attrs)
attrs.update(@reflection.options[:conditions]) if @reflection.options[:conditions].is_a?(Hash)
ensure_owner_is_not_new
record = @reflection.klass.send(:with_scope, :create => construct_scope[:create]) { @reflection.klass.new(attrs) }
record = @reflection.klass.send(:with_scope, :create => construct_scope[:create]) do
@reflection.build_association(attrs)
end
if block_given?
add_record_to_target_with_callbacks(record) { |*block_args| yield(*block_args) }
else
@ -326,7 +410,7 @@ module ActiveRecord
def build_record(attrs)
attrs.update(@reflection.options[:conditions]) if @reflection.options[:conditions].is_a?(Hash)
record = @reflection.klass.new(attrs)
record = @reflection.build_association(attrs)
if block_given?
add_record_to_target_with_callbacks(record) { |*block_args| yield(*block_args) }
else
@ -361,7 +445,8 @@ module ActiveRecord
end
def fetch_first_or_last_using_find?(args)
args.first.kind_of?(Hash) || !(loaded? || @owner.new_record? || @reflection.options[:finder_sql] || !@target.blank? || args.first.kind_of?(Integer))
args.first.kind_of?(Hash) || !(loaded? || @owner.new_record? || @reflection.options[:finder_sql] ||
@target.any? { |record| record.new_record? } || args.first.kind_of?(Integer))
end
end
end

View file

@ -39,7 +39,7 @@ module ActiveRecord
# though the object behind <tt>blog.posts</tt> is not an Array, but an
# ActiveRecord::Associations::HasManyAssociation.
#
# The <tt>@target</tt> object is not loaded until needed. For example,
# The <tt>@target</tt> object is not \loaded until needed. For example,
#
# blog.posts.count
#
@ -57,76 +57,109 @@ module ActiveRecord
reset
end
# Returns the owner of the proxy.
def proxy_owner
@owner
end
# Returns the reflection object that represents the association handled
# by the proxy.
def proxy_reflection
@reflection
end
# Returns the \target of the proxy, same as +target+.
def proxy_target
@target
end
# Does the proxy or its \target respond to +symbol+?
def respond_to?(*args)
proxy_respond_to?(*args) || (load_target && @target.respond_to?(*args))
end
# Explicitly proxy === because the instance method removal above
# doesn't catch it.
# Forwards <tt>===</tt> explicitly to the \target because the instance method
# removal above doesn't catch it. Loads the \target if needed.
def ===(other)
load_target
other === @target
end
# Returns the name of the table of the related class:
#
# post.comments.aliased_table_name # => "comments"
#
def aliased_table_name
@reflection.klass.table_name
end
# Returns the SQL string that corresponds to the <tt>:conditions</tt>
# option of the macro, if given, or +nil+ otherwise.
def conditions
@conditions ||= interpolate_sql(sanitize_sql(@reflection.options[:conditions])) if @reflection.options[:conditions]
@conditions ||= interpolate_sql(@reflection.sanitized_conditions) if @reflection.sanitized_conditions
end
alias :sql_conditions :conditions
# Resets the \loaded flag to +false+ and sets the \target to +nil+.
def reset
@loaded = false
@target = nil
end
# Reloads the \target and returns +self+ on success.
def reload
reset
load_target
self unless @target.nil?
end
# Has the \target been already \loaded?
def loaded?
@loaded
end
# Asserts the \target has been loaded setting the \loaded flag to +true+.
def loaded
@loaded = true
end
# Returns the target of this proxy, same as +proxy_target+.
def target
@target
end
# Sets the target of this proxy to <tt>\target</tt>, and the \loaded flag to +true+.
def target=(target)
@target = target
loaded
end
# Forwards the call to the target. Loads the \target if needed.
def inspect
load_target
@target.inspect
end
def send(method, *args)
if proxy_respond_to?(method)
super
else
load_target
@target.send(method, *args)
end
end
protected
# Does the association have a <tt>:dependent</tt> option?
def dependent?
@reflection.options[:dependent]
end
# Returns a string with the IDs of +records+ joined with a comma, quoted
# if needed. The result is ready to be inserted into a SQL IN clause.
#
# quoted_record_ids(records) # => "23,56,58,67"
#
def quoted_record_ids(records)
records.map { |record| record.quoted_id }.join(',')
end
@ -135,10 +168,13 @@ module ActiveRecord
@owner.send(:interpolate_sql, sql, record)
end
# Forwards the call to the reflection class.
def sanitize_sql(sql)
@reflection.klass.send(:sanitize_sql, sql)
end
# Assigns the ID of the owner to the corresponding foreign key in +record+.
# If the association is polymorphic the type of the owner is also set.
def set_belongs_to_association_for(record)
if @reflection.options[:as]
record["#{@reflection.options[:as]}_id"] = @owner.id unless @owner.new_record?
@ -148,6 +184,7 @@ module ActiveRecord
end
end
# Merges into +options+ the ones coming from the reflection.
def merge_options_from_reflection!(options)
options.reverse_merge!(
:group => @reflection.options[:group],
@ -160,13 +197,17 @@ module ActiveRecord
)
end
# Forwards +with_scope+ to the reflection.
def with_scope(*args, &block)
@reflection.klass.send :with_scope, *args, &block
end
private
# Forwards any missing method call to the \target.
def method_missing(method, *args)
if load_target
raise NoMethodError unless @target.respond_to?(method)
if block_given?
@target.send(method, *args) { |*block_args| yield(*block_args) }
else
@ -175,16 +216,16 @@ module ActiveRecord
end
end
# Loads the target if needed and returns it.
# Loads the \target if needed and returns it.
#
# This method is abstract in the sense that it relies on +find_target+,
# which is expected to be provided by descendants.
#
# If the target is already loaded it is just returned. Thus, you can call
# +load_target+ unconditionally to get the target.
# If the \target is already \loaded it is just returned. Thus, you can call
# +load_target+ unconditionally to get the \target.
#
# ActiveRecord::RecordNotFound is rescued within the method, and it is
# not reraised. The proxy is reset and +nil+ is the return value.
# not reraised. The proxy is \reset and +nil+ is the return value.
def load_target
return nil unless defined?(@loaded)
@ -198,22 +239,33 @@ module ActiveRecord
reset
end
# Can be overwritten by associations that might have the foreign key available for an association without
# having the object itself (and still being a new record). Currently, only belongs_to presents this scenario.
# Can be overwritten by associations that might have the foreign key
# available for an association without having the object itself (and
# still being a new record). Currently, only +belongs_to+ presents
# this scenario (both vanilla and polymorphic).
def foreign_key_present
false
end
# Raises ActiveRecord::AssociationTypeMismatch unless +record+ is of
# the kind of the class of the associated objects. Meant to be used as
# a sanity check when you are about to assign an associated record.
def raise_on_type_mismatch(record)
unless record.is_a?(@reflection.klass)
unless record.is_a?(@reflection.klass) || record.is_a?(@reflection.class_name.constantize)
message = "#{@reflection.class_name}(##{@reflection.klass.object_id}) expected, got #{record.class}(##{record.class.object_id})"
raise ActiveRecord::AssociationTypeMismatch, message
end
end
# Array#flatten has problems with recursive arrays. Going one level deeper solves the majority of the problems.
# Array#flatten has problems with recursive arrays. Going one level
# deeper solves the majority of the problems.
def flatten_deeper(array)
array.collect { |element| element.respond_to?(:flatten) ? element.flatten : element }.flatten
array.collect { |element| (element.respond_to?(:flatten) && !element.is_a?(Hash)) ? element.flatten : element }.flatten
end
# Returns the ID of the owner, quoted if needed.
def owner_quoted_id
@owner.quoted_id
end
end
end

View file

@ -2,11 +2,11 @@ module ActiveRecord
module Associations
class BelongsToAssociation < AssociationProxy #:nodoc:
def create(attributes = {})
replace(@reflection.klass.create(attributes))
replace(@reflection.create_association(attributes))
end
def build(attributes = {})
replace(@reflection.klass.new(attributes))
replace(@reflection.build_association(attributes))
end
def replace(record)

View file

@ -37,7 +37,7 @@ module ActiveRecord
attributes = columns.inject({}) do |attrs, column|
case column.name.to_s
when @reflection.primary_key_name.to_s
attrs[column.name] = @owner.quoted_id
attrs[column.name] = owner_quoted_id
when @reflection.association_foreign_key.to_s
attrs[column.name] = record.quoted_id
else
@ -64,7 +64,7 @@ module ActiveRecord
records.each { |record| @owner.connection.delete(interpolate_sql(sql, record)) }
else
ids = quoted_record_ids(records)
sql = "DELETE FROM #{@owner.connection.quote_table_name @reflection.options[:join_table]} WHERE #{@reflection.primary_key_name} = #{@owner.quoted_id} AND #{@reflection.association_foreign_key} IN (#{ids})"
sql = "DELETE FROM #{@owner.connection.quote_table_name @reflection.options[:join_table]} WHERE #{@reflection.primary_key_name} = #{owner_quoted_id} AND #{@reflection.association_foreign_key} IN (#{ids})"
@owner.connection.delete(sql)
end
end
@ -73,11 +73,21 @@ module ActiveRecord
if @reflection.options[:finder_sql]
@finder_sql = interpolate_sql(@reflection.options[:finder_sql])
else
@finder_sql = "#{@owner.connection.quote_table_name @reflection.options[:join_table]}.#{@reflection.primary_key_name} = #{@owner.quoted_id} "
@finder_sql = "#{@owner.connection.quote_table_name @reflection.options[:join_table]}.#{@reflection.primary_key_name} = #{owner_quoted_id} "
@finder_sql << " AND (#{conditions})" if conditions
end
@join_sql = "INNER JOIN #{@owner.connection.quote_table_name @reflection.options[:join_table]} ON #{@reflection.quoted_table_name}.#{@reflection.klass.primary_key} = #{@owner.connection.quote_table_name @reflection.options[:join_table]}.#{@reflection.association_foreign_key}"
if @reflection.options[:counter_sql]
@counter_sql = interpolate_sql(@reflection.options[:counter_sql])
elsif @reflection.options[:finder_sql]
# replace the SELECT clause with COUNT(*), preserving any hints within /* ... */
@reflection.options[:counter_sql] = @reflection.options[:finder_sql].sub(/SELECT (\/\*.*?\*\/ )?(.*)\bFROM\b/im) { "SELECT #{$1}COUNT(*) FROM" }
@counter_sql = interpolate_sql(@reflection.options[:counter_sql])
else
@counter_sql = @finder_sql
end
end
def construct_scope

View file

@ -1,33 +1,32 @@
module ActiveRecord
module Associations
# This is the proxy that handles a has many association.
#
# If the association has a <tt>:through</tt> option further specialization
# is provided by its child HasManyThroughAssociation.
class HasManyAssociation < AssociationCollection #:nodoc:
# Count the number of associated records. All arguments are optional.
def count(*args)
if @reflection.options[:counter_sql]
@reflection.klass.count_by_sql(@counter_sql)
elsif @reflection.options[:finder_sql]
@reflection.klass.count_by_sql(@finder_sql)
else
column_name, options = @reflection.klass.send(:construct_count_options_from_args, *args)
options[:conditions] = options[:conditions].blank? ?
@finder_sql :
@finder_sql + " AND (#{sanitize_sql(options[:conditions])})"
options[:include] ||= @reflection.options[:include]
value = @reflection.klass.count(column_name, options)
limit = @reflection.options[:limit]
offset = @reflection.options[:offset]
if limit || offset
[ [value - offset.to_i, 0].max, limit.to_i ].min
protected
def owner_quoted_id
if @reflection.options[:primary_key]
quote_value(@owner.send(@reflection.options[:primary_key]))
else
value
@owner.quoted_id
end
end
end
protected
# Returns the number of records in this collection.
#
# If the association has a counter cache it gets that value. Otherwise
# it will attempt to do a count via SQL, bounded to <tt>:limit</tt> if
# there's one. Some configuration options like :group make it impossible
# to do a SQL count, in those cases the array count will be used.
#
# That does not depend on whether the collection has already been loaded
# or not. The +size+ method is the one that takes the loaded flag into
# account and delegates to +count_records+ if needed.
#
# 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.send(:read_attribute, cached_counter_attribute_name)
@ -62,17 +61,18 @@ module ActiveRecord
record.save
end
# Deletes the records according to the <tt>:dependent</tt> option.
def delete_records(records)
case @reflection.options[:dependent]
when :destroy
records.each(&:destroy)
records.each { |r| r.destroy }
when :delete_all
@reflection.klass.delete(records.map(&:id))
@reflection.klass.delete(records.map { |record| record.id })
else
ids = quoted_record_ids(records)
@reflection.klass.update_all(
"#{@reflection.primary_key_name} = NULL",
"#{@reflection.primary_key_name} = #{@owner.quoted_id} AND #{@reflection.klass.primary_key} IN (#{ids})"
"#{@reflection.primary_key_name} = #{owner_quoted_id} AND #{@reflection.klass.primary_key} IN (#{ids})"
)
end
end
@ -88,12 +88,12 @@ module ActiveRecord
when @reflection.options[:as]
@finder_sql =
"#{@reflection.quoted_table_name}.#{@reflection.options[:as]}_id = #{@owner.quoted_id} AND " +
"#{@reflection.quoted_table_name}.#{@reflection.options[:as]}_id = #{owner_quoted_id} AND " +
"#{@reflection.quoted_table_name}.#{@reflection.options[:as]}_type = #{@owner.class.quote_value(@owner.class.base_class.name.to_s)}"
@finder_sql << " AND (#{conditions})" if conditions
else
@finder_sql = "#{@reflection.quoted_table_name}.#{@reflection.primary_key_name} = #{@owner.quoted_id}"
@finder_sql = "#{@reflection.quoted_table_name}.#{@reflection.primary_key_name} = #{owner_quoted_id}"
@finder_sql << " AND (#{conditions})" if conditions
end

View file

@ -9,15 +9,15 @@ module ActiveRecord
alias_method :new, :build
def create!(attrs = nil)
@reflection.klass.transaction do
self << (object = attrs ? @reflection.klass.send(:with_scope, :create => attrs) { @reflection.klass.create! } : @reflection.klass.create!)
transaction do
self << (object = attrs ? @reflection.klass.send(:with_scope, :create => attrs) { @reflection.create_association! } : @reflection.create_association!)
object
end
end
def create(attrs = nil)
@reflection.klass.transaction do
self << (object = attrs ? @reflection.klass.send(:with_scope, :create => attrs) { @reflection.klass.create } : @reflection.klass.create)
transaction do
self << (object = attrs ? @reflection.klass.send(:with_scope, :create => attrs) { @reflection.create_association } : @reflection.create_association)
object
end
end
@ -31,17 +31,15 @@ module ActiveRecord
return count
end
def count(*args)
column_name, options = @reflection.klass.send(:construct_count_options_from_args, *args)
if @reflection.options[:uniq]
# This is needed because 'SELECT count(DISTINCT *)..' is not valid SQL statement.
column_name = "#{@reflection.quoted_table_name}.#{@reflection.klass.primary_key}" if column_name == :all
options.merge!(:distinct => true)
end
@reflection.klass.send(:with_scope, construct_scope) { @reflection.klass.count(column_name, options) }
end
protected
def target_reflection_has_associated_record?
if @reflection.through_reflection.macro == :belongs_to && @owner[@reflection.through_reflection.primary_key_name].blank?
false
else
true
end
end
def construct_find_options!(options)
options[:select] = construct_select(options[:select])
options[:from] ||= construct_from
@ -57,8 +55,9 @@ module ActiveRecord
return false unless record.save
end
end
klass = @reflection.through_reflection.klass
@owner.send(@reflection.through_reflection.name).proxy_target << klass.send(:with_scope, :create => construct_join_attributes(record)) { klass.create! }
through_reflection = @reflection.through_reflection
klass = through_reflection.klass
@owner.send(@reflection.through_reflection.name).proxy_target << klass.send(:with_scope, :create => construct_join_attributes(record)) { through_reflection.create_association! }
end
# TODO - add dependent option support
@ -70,6 +69,7 @@ module ActiveRecord
end
def find_target
return [] unless target_reflection_has_associated_record?
@reflection.klass.find(:all,
:select => construct_select,
:conditions => construct_conditions,
@ -107,12 +107,14 @@ module ActiveRecord
# Associate attributes pointing to owner, quoted.
def construct_quoted_owner_attributes(reflection)
if as = reflection.options[:as]
{ "#{as}_id" => @owner.quoted_id,
{ "#{as}_id" => owner_quoted_id,
"#{as}_type" => reflection.klass.quote_value(
@owner.class.base_class.name.to_s,
reflection.klass.columns_hash["#{as}_type"]) }
elsif reflection.macro == :belongs_to
{ reflection.klass.primary_key => @owner[reflection.primary_key_name] }
else
{ reflection.primary_key_name => @owner.quoted_id }
{ reflection.primary_key_name => owner_quoted_id }
end
end
@ -183,7 +185,7 @@ module ActiveRecord
when @reflection.options[:finder_sql]
@finder_sql = interpolate_sql(@reflection.options[:finder_sql])
@finder_sql = "#{@reflection.quoted_table_name}.#{@reflection.primary_key_name} = #{@owner.quoted_id}"
@finder_sql = "#{@reflection.quoted_table_name}.#{@reflection.primary_key_name} = #{owner_quoted_id}"
@finder_sql << " AND (#{conditions})" if conditions
else
@finder_sql = construct_conditions

View file

@ -7,15 +7,21 @@ module ActiveRecord
end
def create(attrs = {}, replace_existing = true)
new_record(replace_existing) { |klass| klass.create(attrs) }
new_record(replace_existing) do |reflection|
reflection.create_association(attrs)
end
end
def create!(attrs = {}, replace_existing = true)
new_record(replace_existing) { |klass| klass.create!(attrs) }
new_record(replace_existing) do |reflection|
reflection.create_association!(attrs)
end
end
def build(attrs = {}, replace_existing = true)
new_record(replace_existing) { |klass| klass.new(attrs) }
new_record(replace_existing) do |reflection|
reflection.build_association(attrs)
end
end
def replace(obj, dont_save = false)
@ -47,7 +53,16 @@ module ActiveRecord
return (obj.nil? ? nil : self)
end
end
protected
def owner_quoted_id
if @reflection.options[:primary_key]
@owner.class.quote_value(@owner.send(@reflection.options[:primary_key]))
else
@owner.quoted_id
end
end
private
def find_target
@reflection.klass.find(:first,
@ -63,10 +78,10 @@ module ActiveRecord
case
when @reflection.options[:as]
@finder_sql =
"#{@reflection.quoted_table_name}.#{@reflection.options[:as]}_id = #{@owner.quoted_id} AND " +
"#{@reflection.quoted_table_name}.#{@reflection.options[:as]}_id = #{owner_quoted_id} AND " +
"#{@reflection.quoted_table_name}.#{@reflection.options[:as]}_type = #{@owner.class.quote_value(@owner.class.base_class.name.to_s)}"
else
@finder_sql = "#{@reflection.quoted_table_name}.#{@reflection.primary_key_name} = #{@owner.quoted_id}"
@finder_sql = "#{@reflection.quoted_table_name}.#{@reflection.primary_key_name} = #{owner_quoted_id}"
end
@finder_sql << " AND (#{conditions})" if conditions
end
@ -82,7 +97,9 @@ module ActiveRecord
# instance. Otherwise, if the target has not previously been loaded
# elsewhere, the instance we create will get orphaned.
load_target if replace_existing
record = @reflection.klass.send(:with_scope, :create => construct_scope[:create]) { yield @reflection.klass }
record = @reflection.klass.send(:with_scope, :create => construct_scope[:create]) do
yield @reflection
end
if replace_existing
replace(record, true)

View file

@ -10,7 +10,7 @@ module ActiveRecord
base.attribute_types_cached_by_default = ATTRIBUTE_TYPES_CACHED_BY_DEFAULT
base.cattr_accessor :time_zone_aware_attributes, :instance_writer => false
base.time_zone_aware_attributes = false
base.cattr_accessor :skip_time_zone_conversion_for_attributes, :instance_writer => false
base.class_inheritable_accessor :skip_time_zone_conversion_for_attributes, :instance_writer => false
base.skip_time_zone_conversion_for_attributes = []
end
@ -214,7 +214,7 @@ module ActiveRecord
if logger
logger.warn "Exception occurred during reader method compilation."
logger.warn "Maybe #{attr_name} is not a valid Ruby identifier?"
logger.warn "#{err.message}"
logger.warn err.message
end
end
end
@ -232,6 +232,10 @@ module ActiveRecord
def method_missing(method_id, *args, &block)
method_name = method_id.to_s
if self.class.private_method_defined?(method_name)
raise NoMethodError("Attempt to call private method", method_name, args)
end
# If we haven't generated any methods yet, generate them, then
# see if we've created the method we're looking for.
if !self.class.generated_methods?
@ -330,14 +334,18 @@ module ActiveRecord
end
end
# A Person object with a name attribute can ask <tt>person.respond_to?("name")</tt>,
# <tt>person.respond_to?("name=")</tt>, and <tt>person.respond_to?("name?")</tt>
# A Person object with a name attribute can ask <tt>person.respond_to?(:name)</tt>,
# <tt>person.respond_to?(:name=)</tt>, and <tt>person.respond_to?(:name?)</tt>
# which will all return +true+.
alias :respond_to_without_attributes? :respond_to?
def respond_to?(method, include_priv = false)
def respond_to?(method, include_private_methods = false)
method_name = method.to_s
if super
return true
elsif !include_private_methods && super(method, true)
# If we're here than we haven't found among non-private methods
# but found among all methods. Which means that given method is private.
return false
elsif !self.class.generated_methods?
self.class.define_attribute_methods
if self.class.generated_methods.include?(method_name)

File diff suppressed because it is too large Load diff

View file

@ -14,7 +14,7 @@ module ActiveRecord
#
# The third approach, count using options, accepts an option hash as the only parameter. The options are:
#
# * <tt>:conditions</tt>: An SQL fragment like "administrator = 1" or [ "user_name = ?", username ]. See conditions in the intro.
# * <tt>:conditions</tt>: An SQL fragment like "administrator = 1" or [ "user_name = ?", username ]. See conditions in the intro to ActiveRecord::Base.
# * <tt>:joins</tt>: Either an SQL fragment for additional joins like "LEFT JOIN comments ON comments.post_id = id" (rarely needed)
# or named associations in the same form used for the <tt>:include</tt> option, which will perform an INNER JOIN on the associated table(s).
# If the value is a string, then the records will be returned read-only since they will have attributes that do not correspond to the table's columns.
@ -98,7 +98,7 @@ module ActiveRecord
# end
#
# Options:
# * <tt>:conditions</tt> - An SQL fragment like "administrator = 1" or [ "user_name = ?", username ]. See conditions in the intro.
# * <tt>:conditions</tt> - An SQL fragment like "administrator = 1" or [ "user_name = ?", username ]. See conditions in the intro to ActiveRecord::Base.
# * <tt>:include</tt>: Eager loading, see Associations for details. Since calculations don't load anything, the purpose of this is to access fields on joined tables in your conditions, order, or group clauses.
# * <tt>:joins</tt> - An SQL fragment for additional joins like "LEFT JOIN comments ON comments.post_id = id". (Rarely needed).
# The records will be returned read-only since they will have attributes that do not correspond to the table's columns.
@ -188,7 +188,7 @@ module ActiveRecord
end
joins = ""
add_joins!(joins, options, scope)
add_joins!(joins, options[:joins], scope)
if merged_includes.any?
join_dependency = ActiveRecord::Associations::ClassMethods::JoinDependency.new(self, merged_includes, joins)
@ -217,7 +217,7 @@ module ActiveRecord
sql << " ORDER BY #{options[:order]} " if options[:order]
add_limit!(sql, options, scope)
sql << ') AS #{aggregate_alias}_subquery' if use_workaround
sql << ") #{aggregate_alias}_subquery" if use_workaround
sql
end
@ -266,7 +266,14 @@ module ActiveRecord
# column_alias_for("count(*)") # => "count_all"
# column_alias_for("count", "id") # => "count_id"
def column_alias_for(*keys)
connection.table_alias_for(keys.join(' ').downcase.gsub(/\*/, 'all').gsub(/\W+/, ' ').strip.gsub(/ +/, '_'))
table_name = keys.join(' ')
table_name.downcase!
table_name.gsub!(/\*/, 'all')
table_name.gsub!(/\W+/, ' ')
table_name.strip!
table_name.gsub!(/ +/, '_')
connection.table_alias_for(table_name)
end
def column_for(field)
@ -278,11 +285,15 @@ module ActiveRecord
operation = operation.to_s.downcase
case operation
when 'count' then value.to_i
when 'sum' then value =~ /\./ ? value.to_f : value.to_i
when 'avg' then value && value.to_f
else column ? column.type_cast(value) : value
when 'sum' then type_cast_using_column(value || '0', column)
when 'avg' then value && value.to_d
else type_cast_using_column(value, column)
end
end
def type_cast_using_column(value, column)
column ? column.type_cast(value) : value
end
end
end
end

View file

@ -3,9 +3,9 @@ require 'observer'
module ActiveRecord
# Callbacks are hooks into the lifecycle 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
# 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:
# the <tt>Base#save</tt> call for a new record:
#
# * (-) <tt>save</tt>
# * (-) <tt>valid</tt>
@ -22,7 +22,8 @@ module ActiveRecord
# * (8) <tt>after_save</tt>
#
# That's a total of eight callbacks, which gives you immense power to react and prepare for each state in the
# Active Record lifecycle.
# Active Record lifecycle. The sequence for calling <tt>Base#save</tt> an existing record is similar, except that each
# <tt>_on_create</tt> callback is replaced by the corresponding <tt>_on_update</tt> callback.
#
# Examples:
# class CreditCard < ActiveRecord::Base
@ -50,7 +51,7 @@ module ActiveRecord
#
# == Inheritable callback queues
#
# Besides the overwriteable callback methods, it's also possible to register callbacks through the use of the callback macros.
# Besides the overwritable callback methods, it's also possible to register callbacks through the use of the callback macros.
# Their main advantage is that the macros add behavior into a callback queue that is kept intact down through an inheritance
# hierarchy. Example:
#
@ -161,7 +162,7 @@ module ActiveRecord
# == <tt>before_validation*</tt> returning statements
#
# If the returning value of a +before_validation+ callback can be evaluated to +false+, the process will be aborted and <tt>Base#save</tt> will return +false+.
# If Base#save! is called it will raise a RecordNotSaved exception.
# If Base#save! is called it will raise a ActiveRecord::RecordInvalid exception.
# Nothing will be appended to the errors object.
#
# == Canceling callbacks
@ -169,6 +170,18 @@ module ActiveRecord
# If a <tt>before_*</tt> callback returns +false+, all the later callbacks and the associated action are cancelled. If an <tt>after_*</tt> callback returns
# +false+, all the later callbacks are cancelled. Callbacks are generally run in the order they are defined, with the exception of callbacks
# defined as methods on the model, which are called last.
#
# == Transactions
#
# 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.
#
# 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
# instead of quietly returning +false+.
module Callbacks
CALLBACKS = %w(
after_find after_initialize before_save after_save before_create after_create before_update after_update before_validation
@ -197,6 +210,8 @@ module ActiveRecord
def before_save() end
# Is called _after_ <tt>Base.save</tt> (regardless of whether it's a +create+ or +update+ save).
# Note that this callback is still wrapped in the transaction around +save+. For example, if you
# invoke an external indexer at this point it won't see the changes in the database.
#
# class Contact < ActiveRecord::Base
# after_save { logger.info( 'New contact saved!' ) }
@ -214,6 +229,8 @@ module ActiveRecord
def before_create() end
# Is called _after_ <tt>Base.save</tt> on new objects that haven't been saved yet (no record exists).
# Note that this callback is still wrapped in the transaction around +save+. For example, if you
# invoke an external indexer at this point it won't see the changes in the database.
def after_create() end
def create_with_callbacks #:nodoc:
return false if callback(:before_create) == false
@ -227,6 +244,8 @@ module ActiveRecord
def before_update() end
# Is called _after_ <tt>Base.save</tt> on existing objects that have a record.
# Note that this callback is still wrapped in the transaction around +save+. For example, if you
# invoke an external indexer at this point it won't see the changes in the database.
def after_update() end
def update_with_callbacks(*args) #:nodoc:
@ -262,7 +281,7 @@ module ActiveRecord
def valid_with_callbacks? #:nodoc:
return false if callback(:before_validation) == false
if new_record? then result = callback(:before_validation_on_create) else result = callback(:before_validation_on_update) end
return false if result == false
return false if false == result
result = valid_without_callbacks?
@ -293,14 +312,14 @@ module ActiveRecord
private
def callback(method)
notify(method)
result = run_callbacks(method) { |result, object| result == false }
result = run_callbacks(method) { |result, object| false == result }
if result != false && respond_to_without_attributes?(method)
result = send(method)
end
notify(method)
return result
end

View file

@ -0,0 +1,349 @@
require 'monitor'
require 'set'
module ActiveRecord
# Raised when a connection could not be obtained within the connection
# acquisition timeout period.
class ConnectionTimeoutError < ConnectionNotEstablished
end
module ConnectionAdapters
# Connection pool base class for managing ActiveRecord database
# connections.
#
# == Introduction
#
# A connection pool synchronizes thread access to a limited number of
# database connections. The basic idea is that each thread checks out a
# database connection from the pool, uses that connection, and checks the
# connection back in. ConnectionPool is completely thread-safe, and will
# ensure that a connection cannot be used by two threads at the same time,
# as long as ConnectionPool's contract is correctly followed. It will also
# handle cases in which there are more threads than connections: if all
# connections have been checked out, and a thread tries to checkout a
# connection anyway, then ConnectionPool will wait until some other thread
# has checked in a connection.
#
# == Obtaining (checking out) a connection
#
# Connections can be obtained and used from a connection pool in several
# ways:
#
# 1. Simply use ActiveRecord::Base.connection as with ActiveRecord 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 ActiveRecord when used in conjunction with
# ActionPack's request handling cycle.
# 2. Manually check out a connection from the pool with
# ActiveRecord::Base.connection_pool.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
# obtains a connection, yields it as the sole argument to the block,
# and returns it to the pool after the block completes.
#
# Connections in the pool are actually AbstractAdapter objects (or objects
# compatible with AbstractAdapter's interface).
#
# == Options
#
# There are two connection-pooling-related options that you can add to
# your database connection configuration:
#
# * +pool+: number indicating size of connection pool (default 5)
# * +wait_timeout+: number of seconds to block and wait for a connection
# before giving up and raising a timeout error (default 5 seconds).
class ConnectionPool
attr_reader :spec
# Creates a new ConnectionPool object. +spec+ is a ConnectionSpecification
# object which describes database connection information (e.g. adapter,
# host name, username, password, etc), as well as the maximum size for
# this ConnectionPool.
#
# The default ConnectionPool maximum size is 5.
def initialize(spec)
@spec = spec
# The cache of reserved connections mapped to threads
@reserved_connections = {}
# The mutex used to synchronize pool access
@connection_mutex = Monitor.new
@queue = @connection_mutex.new_cond
# default 5 second timeout
@timeout = spec.config[:wait_timeout] || 5
# default max pool size to 5
@size = (spec.config[:pool] && spec.config[:pool].to_i) || 5
@connections = []
@checked_out = []
end
# Retrieve the connection associated with the current thread, or call
# #checkout to obtain one if necessary.
#
# #connection can be called any number of times; the connection is
# held in a hash keyed by the thread id.
def connection
if conn = @reserved_connections[current_connection_id]
conn
else
@reserved_connections[current_connection_id] = checkout
end
end
# Signal that the thread is finished with the current connection.
# #release_connection releases the connection-thread association
# and returns the connection to the pool.
def release_connection
conn = @reserved_connections.delete(current_connection_id)
checkin conn if conn
end
# Reserve a connection, and yield it to a block. Ensure the connection is
# checked back in when finished.
def with_connection
conn = checkout
yield conn
ensure
checkin conn
end
# Returns true if a connection has already been opened.
def connected?
!@connections.empty?
end
# Disconnects all connections in the pool, and clears the pool.
def disconnect!
@reserved_connections.each do |name,conn|
checkin conn
end
@reserved_connections = {}
@connections.each do |conn|
conn.disconnect!
end
@connections = []
end
# Clears the cache which maps classes
def clear_reloadable_connections!
@reserved_connections.each do |name, conn|
checkin conn
end
@reserved_connections = {}
@connections.each do |conn|
conn.disconnect! if conn.requires_reloading?
end
@connections = []
end
# Verify active connections and remove and disconnect connections
# associated with stale threads.
def verify_active_connections! #:nodoc:
clear_stale_cached_connections!
@connections.each do |connection|
connection.verify!
end
end
# Return any checked-out connections back to the pool by threads that
# are no longer alive.
def clear_stale_cached_connections!
remove_stale_cached_threads!(@reserved_connections) do |name, conn|
checkin conn
end
end
# Check-out a database connection from the pool, indicating that you want
# to use it. You should call #checkin when you no longer need this.
#
# This is done by either returning an existing connection, or by creating
# a new connection. If the maximum number of connections for this pool has
# already been reached, but the pool is empty (i.e. they're all being used),
# then this method will wait until a thread has checked in a connection.
# The wait time is bounded however: if no connection can be checked out
# within the timeout specified for this pool, then a ConnectionTimeoutError
# exception will be raised.
#
# Returns: an AbstractAdapter object.
#
# Raises:
# - ConnectionTimeoutError: no connection can be obtained from the pool
# within the timeout period.
def checkout
# Checkout an available connection
@connection_mutex.synchronize do
loop do
conn = if @checked_out.size < @connections.size
checkout_existing_connection
elsif @connections.size < @size
checkout_new_connection
end
return conn if conn
# No connections available; wait for one
if @queue.wait(@timeout)
next
else
# try looting dead threads
clear_stale_cached_connections!
if @size == @checked_out.size
raise ConnectionTimeoutError, "could not obtain a database connection within #{@timeout} seconds. The pool size is currently #{@size}, perhaps you need to increase it?"
end
end
end
end
end
# Check-in a database connection back into the pool, indicating that you
# no longer need this connection.
#
# +conn+: an AbstractAdapter object, which was obtained by earlier by
# calling +checkout+ on this pool.
def checkin(conn)
@connection_mutex.synchronize do
conn.run_callbacks :checkin
@checked_out.delete conn
@queue.signal
end
end
synchronize :clear_reloadable_connections!, :verify_active_connections!,
:connected?, :disconnect!, :with => :@connection_mutex
private
def new_connection
ActiveRecord::Base.send(spec.adapter_method, spec.config)
end
def current_connection_id #:nodoc:
Thread.current.object_id
end
# Remove stale threads from the cache.
def remove_stale_cached_threads!(cache, &block)
keys = Set.new(cache.keys)
Thread.list.each do |thread|
keys.delete(thread.object_id) if thread.alive?
end
keys.each do |key|
next unless cache.has_key?(key)
block.call(key, cache[key])
cache.delete(key)
end
end
def checkout_new_connection
c = new_connection
@connections << c
checkout_and_verify(c)
end
def checkout_existing_connection
c = (@connections - @checked_out).first
checkout_and_verify(c)
end
def checkout_and_verify(c)
c.verify!
c.run_callbacks :checkout
@checked_out << c
c
end
end
# ConnectionHandler is a collection of ConnectionPool objects. It is used
# for keeping separate connection pools for ActiveRecord models that connect
# to different databases.
#
# For example, suppose that you have 5 models, with the following hierarchy:
#
# |
# +-- Book
# | |
# | +-- ScaryBook
# | +-- GoodBook
# +-- Author
# +-- BankAccount
#
# Suppose that Book is to connect to a separate database (i.e. one other
# than the default database). Then Book, ScaryBook and GoodBook will all use
# the same connection pool. Likewise, Author and BankAccount will use the
# same connection pool. However, the connection pool used by Author/BankAccount
# is not the same as the one used by Book/ScaryBook/GoodBook.
#
# Normally there is only a single ConnectionHandler instance, accessible via
# ActiveRecord::Base.connection_handler. ActiveRecord models use this to
# determine that connection pool that they should use.
class ConnectionHandler
def initialize(pools = {})
@connection_pools = pools
end
def connection_pools
@connection_pools ||= {}
end
def establish_connection(name, spec)
@connection_pools[name] = ConnectionAdapters::ConnectionPool.new(spec)
end
# Returns any connections in use by the current thread back to the pool,
# and also returns connections to the pool cached by threads that are no
# longer alive.
def clear_active_connections!
@connection_pools.each_value do |pool|
pool.release_connection
pool.clear_stale_cached_connections!
end
end
# Clears the cache which maps classes
def clear_reloadable_connections!
@connection_pools.each_value {|pool| pool.clear_reloadable_connections! }
end
def clear_all_connections!
@connection_pools.each_value {|pool| pool.disconnect! }
end
# Verify active connections.
def verify_active_connections! #:nodoc:
@connection_pools.each_value {|pool| pool.verify_active_connections! }
end
# Locate the connection of the nearest super class. This can be an
# 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)
(pool && pool.connection) or raise ConnectionNotEstablished
end
# Returns true if a connection that's accessible to this class has
# already been opened.
def connected?(klass)
retrieve_connection_pool(klass).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
# re-establishing the connection.
def remove_connection(klass)
pool = @connection_pools[klass.name]
@connection_pools.delete_if { |key, value| value == pool }
pool.disconnect! if pool
pool.spec.config if pool
end
def retrieve_connection_pool(klass)
pool = @connection_pools[klass.name]
return pool if pool
return nil if ActiveRecord::Base == klass
retrieve_connection_pool klass.superclass
end
end
end
end

View file

@ -1,5 +1,3 @@
require 'set'
module ActiveRecord
class Base
class ConnectionSpecification #:nodoc:
@ -9,163 +7,9 @@ module ActiveRecord
end
end
# Check for activity after at least +verification_timeout+ seconds.
# Defaults to 0 (always check.)
cattr_accessor :verification_timeout, :instance_writer => false
@@verification_timeout = 0
# The class -> [adapter_method, config] map
@@defined_connections = {}
# The class -> thread id -> adapter cache. (class -> adapter if not allow_concurrency)
@@active_connections = {}
class << self
# Retrieve the connection cache.
def thread_safe_active_connections #:nodoc:
@@active_connections[Thread.current.object_id] ||= {}
end
def single_threaded_active_connections #:nodoc:
@@active_connections
end
# pick up the right active_connection method from @@allow_concurrency
if @@allow_concurrency
alias_method :active_connections, :thread_safe_active_connections
else
alias_method :active_connections, :single_threaded_active_connections
end
# set concurrency support flag (not thread safe, like most of the methods in this file)
def allow_concurrency=(threaded) #:nodoc:
logger.debug "allow_concurrency=#{threaded}" if logger
return if @@allow_concurrency == threaded
clear_all_cached_connections!
@@allow_concurrency = threaded
method_prefix = threaded ? "thread_safe" : "single_threaded"
sing = (class << self; self; end)
[:active_connections, :scoped_methods].each do |method|
sing.send(:alias_method, method, "#{method_prefix}_#{method}")
end
log_connections if logger
end
def active_connection_name #:nodoc:
@active_connection_name ||=
if active_connections[name] || @@defined_connections[name]
name
elsif self == ActiveRecord::Base
nil
else
superclass.active_connection_name
end
end
def clear_active_connection_name #:nodoc:
@active_connection_name = nil
subclasses.each { |klass| klass.clear_active_connection_name }
end
# Returns the connection currently associated with the class. This can
# also be used to "borrow" the connection to do database work unrelated
# to any of the specific Active Records.
def connection
if defined?(@active_connection_name) && (conn = active_connections[@active_connection_name])
conn
else
# retrieve_connection sets the cache key.
conn = retrieve_connection
active_connections[@active_connection_name] = conn
end
end
# Clears the cache which maps classes to connections.
def clear_active_connections!
clear_cache!(@@active_connections) do |name, conn|
conn.disconnect!
end
end
# Clears the cache which maps classes
def clear_reloadable_connections!
if @@allow_concurrency
# With concurrent connections @@active_connections is
# a hash keyed by thread id.
@@active_connections.each do |thread_id, conns|
conns.each do |name, conn|
if conn.requires_reloading?
conn.disconnect!
@@active_connections[thread_id].delete(name)
end
end
end
else
@@active_connections.each do |name, conn|
if conn.requires_reloading?
conn.disconnect!
@@active_connections.delete(name)
end
end
end
end
# Verify active connections.
def verify_active_connections! #:nodoc:
if @@allow_concurrency
remove_stale_cached_threads!(@@active_connections) do |name, conn|
conn.disconnect!
end
end
active_connections.each_value do |connection|
connection.verify!(@@verification_timeout)
end
end
private
def clear_cache!(cache, thread_id = nil, &block)
if cache
if @@allow_concurrency
thread_id ||= Thread.current.object_id
thread_cache, cache = cache, cache[thread_id]
return unless cache
end
cache.each(&block) if block_given?
cache.clear
end
ensure
if thread_cache && @@allow_concurrency
thread_cache.delete(thread_id)
end
end
# Remove stale threads from the cache.
def remove_stale_cached_threads!(cache, &block)
stale = Set.new(cache.keys)
Thread.list.each do |thread|
stale.delete(thread.object_id) if thread.alive?
end
stale.each do |thread_id|
clear_cache!(cache, thread_id, &block)
end
end
def clear_all_cached_connections!
if @@allow_concurrency
@@active_connections.each_value do |connection_hash_for_thread|
connection_hash_for_thread.each_value {|conn| conn.disconnect! }
connection_hash_for_thread.clear
end
else
@@active_connections.each_value {|conn| conn.disconnect! }
end
@@active_connections.clear
end
end
# The connection handler
cattr_accessor :connection_handler, :instance_writer => false
@@connection_handler = ConnectionAdapters::ConnectionHandler.new
# Returns the connection currently associated with the class. This can
# also be used to "borrow" the connection to do database work that isn't
@ -208,9 +52,7 @@ module ActiveRecord
raise AdapterNotSpecified unless defined? RAILS_ENV
establish_connection(RAILS_ENV)
when ConnectionSpecification
clear_active_connection_name
@active_connection_name = name
@@defined_connections[name] = spec
@@connection_handler.establish_connection(name, spec)
when Symbol, String
if configuration = configurations[spec.to_s]
establish_connection(configuration)
@ -243,67 +85,52 @@ module ActiveRecord
end
end
# Locate the connection of the nearest super class. This can be an
# 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 self.retrieve_connection #:nodoc:
# Name is nil if establish_connection hasn't been called for
# some class along the inheritance chain up to AR::Base yet.
if name = active_connection_name
if conn = active_connections[name]
# Verify the connection.
conn.verify!(@@verification_timeout)
elsif spec = @@defined_connections[name]
# Activate this connection specification.
klass = name.constantize
klass.connection = spec
conn = active_connections[name]
end
class << self
# Deprecated and no longer has any effect.
def allow_concurrency
ActiveSupport::Deprecation.warn("ActiveRecord::Base.allow_concurrency has been deprecated and no longer has any effect. Please remove all references to allow_concurrency.")
end
conn or raise ConnectionNotEstablished
end
# Returns true if a connection that's accessible to this class has already been opened.
def self.connected?
active_connections[active_connection_name] ? true : false
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
# re-establishing the connection.
def self.remove_connection(klass=self)
spec = @@defined_connections[klass.name]
konn = active_connections[klass.name]
@@defined_connections.delete_if { |key, value| value == spec }
active_connections.delete_if { |key, value| value == konn }
konn.disconnect! if konn
spec.config if spec
end
# Set the connection for the class.
def self.connection=(spec) #:nodoc:
if spec.kind_of?(ActiveRecord::ConnectionAdapters::AbstractAdapter)
active_connections[name] = spec
elsif spec.kind_of?(ConnectionSpecification)
config = spec.config.reverse_merge(:allow_concurrency => @@allow_concurrency)
self.connection = self.send(spec.adapter_method, config)
elsif spec.nil?
raise ConnectionNotEstablished
else
establish_connection spec
# Deprecated and no longer has any effect.
def allow_concurrency=(flag)
ActiveSupport::Deprecation.warn("ActiveRecord::Base.allow_concurrency= has been deprecated and no longer has any effect. Please remove all references to allow_concurrency=.")
end
end
# connection state logging
def self.log_connections #:nodoc:
if logger
logger.info "Defined connections: #{@@defined_connections.inspect}"
logger.info "Active connections: #{active_connections.inspect}"
logger.info "Active connection name: #{@active_connection_name}"
# Deprecated and no longer has any effect.
def verification_timeout
ActiveSupport::Deprecation.warn("ActiveRecord::Base.verification_timeout has been deprecated and no longer has any effect. Please remove all references to verification_timeout.")
end
# Deprecated and no longer has any effect.
def verification_timeout=(flag)
ActiveSupport::Deprecation.warn("ActiveRecord::Base.verification_timeout= has been deprecated and no longer has any effect. Please remove all references to verification_timeout=.")
end
# Returns the connection currently associated with the class. This can
# also be used to "borrow" the connection to do database work unrelated
# to any of the specific Active Records.
def connection
retrieve_connection
end
def connection_pool
connection_handler.retrieve_connection_pool(self)
end
def retrieve_connection
connection_handler.retrieve_connection(self)
end
def connected?
connection_handler.connected?(self)
end
def remove_connection(klass = self)
connection_handler.remove_connection(klass)
end
delegate :clear_active_connections!, :clear_reloadable_connections!,
:clear_all_connections!,:verify_active_connections!, :to => :connection_handler
end
end
end

View file

@ -98,8 +98,14 @@ module ActiveRecord
add_limit_offset!(sql, options) if options
end
# Appends +LIMIT+ and +OFFSET+ options to an SQL statement.
# Appends +LIMIT+ and +OFFSET+ options to an SQL statement, or some SQL
# fragment that has the same semantics as LIMIT and OFFSET.
#
# +options+ must be a Hash which contains a +:limit+ option (required)
# and an +:offset+ option (optional).
#
# This method *modifies* the +sql+ parameter.
#
# ===== Examples
# add_limit_offset!('SELECT * FROM suppliers', {:limit => 10, :offset => 50})
# generates
@ -114,10 +120,6 @@ module ActiveRecord
sql
end
def sanitize_limit(limit)
limit.to_s[/,/] ? limit.split(',').map{ |i| i.to_i }.join(',') : limit.to_i
end
# Appends a locking clause to an SQL statement.
# This method *modifies* the +sql+ parameter.
# # SELECT * FROM suppliers FOR UPDATE
@ -149,6 +151,14 @@ module ActiveRecord
"INSERT INTO #{quote_table_name(table_name)} VALUES(DEFAULT)"
end
def case_sensitive_equality_operator
"="
end
def limited_update_conditions(where_sql, quoted_table_name, quoted_primary_key)
"WHERE #{quoted_primary_key} IN (SELECT #{quoted_primary_key} FROM #{quoted_table_name} #{where_sql})"
end
protected
# Returns an array of record hashes with the column names as keys and
# column values as values.
@ -171,6 +181,21 @@ module ActiveRecord
def delete_sql(sql, name = nil)
update_sql(sql, name)
end
# Sanitizes the given LIMIT parameter in order to prevent SQL injection.
#
# +limit+ may be anything that can evaluate to a string via #to_s. It
# should look like an integer, or a comma-delimited list of integers.
#
# Returns the sanitized limit parameter, either as an integer, or as a
# string which contains a comma-delimited list of integers.
def sanitize_limit(limit)
if limit.to_s =~ /,/
limit.to_s.split(',').map{ |i| i.to_i }.join(',')
else
limit.to_i
end
end
end
end
end

View file

@ -4,7 +4,6 @@ module ActiveRecord
class << self
def included(base)
base.class_eval do
attr_accessor :query_cache_enabled
alias_method_chain :columns, :query_cache
alias_method_chain :select_all, :query_cache
end
@ -16,7 +15,7 @@ module ActiveRecord
method_names.each do |method_name|
base.class_eval <<-end_code, __FILE__, __LINE__
def #{method_name}_with_query_dirty(*args)
clear_query_cache if @query_cache_enabled
clear_query_cache if query_cache_enabled
#{method_name}_without_query_dirty(*args)
end
@ -26,22 +25,38 @@ module ActiveRecord
end
end
def query_cache_enabled
Thread.current['query_cache_enabled']
end
def query_cache_enabled=(flag)
Thread.current['query_cache_enabled'] = flag
end
def query_cache
Thread.current['query_cache']
end
def query_cache=(cache)
Thread.current['query_cache'] = cache
end
# Enable the query cache within the block.
def cache
old, @query_cache_enabled = @query_cache_enabled, true
@query_cache ||= {}
old, self.query_cache_enabled = query_cache_enabled, true
self.query_cache ||= {}
yield
ensure
clear_query_cache
@query_cache_enabled = old
self.query_cache_enabled = old
end
# Disable the query cache within the block.
def uncached
old, @query_cache_enabled = @query_cache_enabled, false
old, self.query_cache_enabled = query_cache_enabled, false
yield
ensure
@query_cache_enabled = old
self.query_cache_enabled = old
end
# Clears the query cache.
@ -51,11 +66,11 @@ module ActiveRecord
# the same SQL query and repeatedly return the same result each time, silently
# undermining the randomness you were expecting.
def clear_query_cache
@query_cache.clear if @query_cache
query_cache.clear if query_cache
end
def select_all_with_query_cache(*args)
if @query_cache_enabled
if query_cache_enabled
cache_sql(args.first) { select_all_without_query_cache(*args) }
else
select_all_without_query_cache(*args)
@ -63,8 +78,8 @@ module ActiveRecord
end
def columns_with_query_cache(*args)
if @query_cache_enabled
@query_cache["SHOW FIELDS FROM #{args.first}"] ||= columns_without_query_cache(*args)
if query_cache_enabled
query_cache["SHOW FIELDS FROM #{args.first}"] ||= columns_without_query_cache(*args)
else
columns_without_query_cache(*args)
end
@ -73,11 +88,11 @@ module ActiveRecord
private
def cache_sql(sql)
result =
if @query_cache.has_key?(sql)
if query_cache.has_key?(sql)
log_info(sql, "CACHE", 0.0)
@query_cache[sql]
query_cache[sql]
else
@query_cache[sql] = yield
query_cache[sql] = yield
end
if Array === result

View file

@ -1,4 +1,5 @@
require 'date'
require 'set'
require 'bigdecimal'
require 'bigdecimal/util'
@ -6,6 +7,8 @@ module ActiveRecord
module ConnectionAdapters #:nodoc:
# An abstract definition of a column in a table.
class Column
TRUE_VALUES = [true, 1, '1', 't', 'T', 'true', 'TRUE'].to_set
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/
@ -30,11 +33,15 @@ module ActiveRecord
end
def text?
[:string, :text].include? type
type == :string || type == :text
end
def number?
[:float, :integer, :decimal].include? type
type == :integer || type == :float || type == :decimal
end
def has_default?
!default.nil?
end
# Returns the Ruby class that corresponds to the abstract data type.
@ -135,10 +142,10 @@ module ActiveRecord
# convert something to a boolean
def value_to_boolean(value)
if value == true || value == false
value
if value.is_a?(String) && value.blank?
nil
else
%w(true t 1).include?(value.to_s.downcase)
TRUE_VALUES.include?(value)
end
end
@ -249,6 +256,10 @@ module ActiveRecord
class IndexDefinition < Struct.new(:table, :name, :unique, :columns) #: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(:base, :name, :type, :limit, :precision, :scale, :default, :null) #:nodoc:
def sql_type
@ -272,9 +283,29 @@ module ActiveRecord
end
end
# Represents a SQL table in an abstract way.
# Columns are stored as a ColumnDefinition in the +columns+ attribute.
# 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+ and
# +change_table+ is actually of this type:
#
# class SomeMigration < ActiveRecord::Migration
# def self.up
# create_table :foo do |t|
# puts t.class # => "ActiveRecord::ConnectionAdapters::TableDefinition"
# end
# end
#
# def self.down
# ...
# end
# end
#
# The table definitions
# The Columns are stored as a ColumnDefinition in the +columns+ attribute.
class TableDefinition
# An array of ColumnDefinition objects, representing the column changes
# that have been defined.
attr_accessor :columns
def initialize(base)
@ -318,6 +349,12 @@ module ActiveRecord
# * <tt>:scale</tt> -
# Specifies the scale for a <tt>:decimal</tt> column.
#
# For clarity's sake: the precision is the number of significant digits,
# while 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> <=
@ -371,6 +408,10 @@ module ActiveRecord
# 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
#
# == Short-hand examples
#
# Instead of calling +column+ directly, you can also work with the short-hand definitions for the default types.

View file

@ -31,19 +31,25 @@ module ActiveRecord
# See the concrete implementation for details on the expected parameter values.
def columns(table_name, name = nil) end
# Creates a new table
# 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
# form or the regular form, like this:
#
# === Block form
# # create_table() yields a TableDefinition instance
# # create_table() passes a TableDefinition object to the block.
# # This form will not only create the table, but also columns for the
# # table.
# create_table(:suppliers) do |t|
# t.column :name, :string, :limit => 60
# # Other fields here
# end
#
# === Regular form
# # Creates a table called 'suppliers' with no columns.
# create_table(:suppliers)
# # Add a column to 'suppliers'.
# add_column(:suppliers, :name, :string, {:limit => 60})
#
# The +options+ hash can include the following keys:
@ -356,7 +362,7 @@ module ActiveRecord
def type_to_sql(type, limit = nil, precision = nil, scale = nil) #:nodoc:
if native = native_database_types[type]
column_type_sql = native.is_a?(Hash) ? native[:name] : native
column_type_sql = (native.is_a?(Hash) ? native[:name] : native).dup
if type == :decimal # ignore limit, use precision and scale
scale ||= native[:scale]
@ -371,7 +377,7 @@ module ActiveRecord
raise ArgumentError, "Error adding decimal column: precision cannot be empty if scale if specified"
end
elsif limit ||= native.is_a?(Hash) && native[:limit]
elsif (type != :primary_key) && (limit ||= native.is_a?(Hash) && native[:limit])
column_type_sql << "(#{limit})"
end

View file

@ -7,23 +7,31 @@ require 'active_record/connection_adapters/abstract/schema_definitions'
require 'active_record/connection_adapters/abstract/schema_statements'
require 'active_record/connection_adapters/abstract/database_statements'
require 'active_record/connection_adapters/abstract/quoting'
require 'active_record/connection_adapters/abstract/connection_pool'
require 'active_record/connection_adapters/abstract/connection_specification'
require 'active_record/connection_adapters/abstract/query_cache'
module ActiveRecord
module ConnectionAdapters # :nodoc:
# All the concrete database adapters follow the interface laid down in this class.
# You can use this interface directly by borrowing the database connection from the Base with
# Base.connection.
# ActiveRecord supports multiple database systems. AbstractAdapter and
# related classes form the abstraction layer which makes this possible.
# An AbstractAdapter represents a connection to a database, and provides an
# abstract interface for database-specific functionality such as establishing
# a connection, escaping values, building the right SQL fragments for ':offset'
# and ':limit' options, etc.
#
# Most of the methods in the adapter are useful during migrations. Most
# notably, SchemaStatements#create_table, SchemaStatements#drop_table,
# SchemaStatements#add_index, SchemaStatements#remove_index,
# SchemaStatements#add_column, SchemaStatements#change_column and
# SchemaStatements#remove_column are very useful.
# All the concrete database adapters follow the interface laid down in this class.
# ActiveRecord::Base.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.
class AbstractAdapter
include Quoting, DatabaseStatements, SchemaStatements
include QueryCache
include ActiveSupport::Callbacks
define_callbacks :checkout, :checkin
checkout :reset!
@@row_even = true
def initialize(connection, logger = nil) #:nodoc:
@ -51,6 +59,13 @@ module ActiveRecord
true
end
# Does this adapter support DDL rollbacks in transactions? That is, would
# CREATE TABLE or ALTER TABLE get rolled back by a transaction? PostgreSQL,
# SQL Server, and others support this. MySQL and others do not.
def supports_ddl_transactions?
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.
@ -80,48 +95,75 @@ module ActiveRecord
# CONNECTION MANAGEMENT ====================================
# Is this connection active and ready to perform queries?
# Checks whether the connection to the database is still active. This includes
# checking whether the database is actually capable of responding, i.e. whether
# the connection isn't stale.
def active?
@active != false
end
# Close this connection and open a new one in its place.
# Disconnects from the database if already connected, and establishes a
# new connection with the database.
def reconnect!
@active = true
end
# Close this connection
# Disconnects from the database if already connected. Otherwise, this
# method does nothing.
def disconnect!
@active = false
end
# Reset the state of this connection, directing the DBMS to clear
# transactions and other connection-related server-side state. Usually a
# database-dependent operation.
#
# The default implementation does nothing; the implementation should be
# overridden by concrete adapters.
def reset!
# this should be overridden by concrete adapters
end
# Returns true if its safe to reload the connection between requests for development mode.
# This is not the case for Ruby/MySQL and it's not necessary for any adapters except SQLite.
def requires_reloading?
false
end
# Lazily verify this connection, calling <tt>active?</tt> only if it hasn't
# been called for +timeout+ seconds.
def verify!(timeout)
now = Time.now.to_i
if (now - @last_verification) > timeout
reconnect! unless active?
@last_verification = now
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
# 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 connection. Useful for
# when you need to call a proprietary method such as postgresql's lo_*
# methods
# Provides access to the underlying database driver for this adapter. For
# example, this method returns a Mysql object in case of MysqlAdapter,
# and a PGconn object in case of PostgreSQLAdapter.
#
# This is useful for when you need to call a proprietary method such as
# PostgreSQL's lo_* methods.
def raw_connection
@connection
end
def log_info(sql, name, runtime)
def open_transactions
@open_transactions ||= 0
end
def increment_open_transactions
@open_transactions ||= 0
@open_transactions += 1
end
def decrement_open_transactions
@open_transactions -= 1
end
def log_info(sql, name, seconds)
if @logger && @logger.debug?
name = "#{name.nil? ? "SQL" : name} (#{sprintf("%f", runtime)})"
@logger.debug format_log_entry(name, sql.squeeze(' '))
name = "#{name.nil? ? "SQL" : name} (#{sprintf("%.1f", seconds * 1000)}ms)"
@logger.debug(format_log_entry(name, sql.squeeze(' ')))
end
end

View file

@ -42,27 +42,6 @@ end
module ActiveRecord
class Base
def self.require_mysql
# Include the MySQL driver if one hasn't already been loaded
unless defined? Mysql
begin
require_library_or_gem 'mysql'
rescue LoadError => cannot_require_mysql
# Use the bundled Ruby/MySQL driver if no driver is already in place
begin
ActiveSupport::Deprecation.warn "You're using the Ruby-based MySQL library that ships with Rails. This library will be REMOVED FROM RAILS 2.2. Please switch to the offical mysql gem: `gem install mysql`", caller
require 'active_record/vendor/mysql'
rescue LoadError
raise cannot_require_mysql
end
end
end
# Define Mysql::Result.all_hashes
MysqlCompat.define_all_hashes_method!
end
# Establishes a connection to the database that's used by all Active Record objects.
def self.mysql_connection(config) # :nodoc:
config = config.symbolize_keys
@ -78,9 +57,19 @@ module ActiveRecord
raise ArgumentError, "No database specified. Missing argument: database."
end
require_mysql
# Require the MySQL driver and define Mysql::Result.all_hashes
unless defined? Mysql
begin
require_library_or_gem('mysql')
rescue LoadError
$stderr.puts '!!! The bundled mysql.rb driver has been removed from Rails 2.2. Please install the mysql gem and try again: gem install mysql.'
raise
end
end
MysqlCompat.define_all_hashes_method!
mysql = Mysql.init
mysql.ssl_set(config[:sslkey], config[:sslcert], config[:sslca], config[:sslcapath], config[:sslcipher]) if config[:sslkey]
mysql.ssl_set(config[:sslkey], config[:sslcert], config[:sslca], config[:sslcapath], config[:sslcipher]) if config[:sslca] || config[:sslkey]
ConnectionAdapters::MysqlAdapter.new(mysql, logger, [host, username, password, database, port, socket], config)
end
@ -91,7 +80,7 @@ module ActiveRecord
def extract_default(default)
if type == :binary || type == :text
if default.blank?
nil
return null ? nil : ''
else
raise ArgumentError, "#{type} columns cannot have a default value: #{default.inspect}"
end
@ -102,6 +91,11 @@ module ActiveRecord
end
end
def has_default?
return false if type == :binary || type == :text #mysql forbids defaults on blob and text columns
super
end
private
def simplified_type(field_type)
return :boolean if MysqlAdapter.emulate_booleans && field_type.downcase.index("tinyint(1)")
@ -156,6 +150,7 @@ module ActiveRecord
# * <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>: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.
@ -168,8 +163,10 @@ module ActiveRecord
#
# ActiveRecord::ConnectionAdapters::MysqlAdapter.emulate_booleans = false
class MysqlAdapter < AbstractAdapter
@@emulate_booleans = true
cattr_accessor :emulate_booleans
self.emulate_booleans = true
ADAPTER_NAME = 'MySQL'.freeze
LOST_CONNECTION_ERROR_MESSAGES = [
"Server shutdown in progress",
@ -177,7 +174,22 @@ module ActiveRecord
"Lost connection to MySQL server during query",
"MySQL server has gone away" ]
QUOTED_TRUE, QUOTED_FALSE = '1', '0'
QUOTED_TRUE, QUOTED_FALSE = '1'.freeze, '0'.freeze
NATIVE_DATABASE_TYPES = {
:primary_key => "int(11) DEFAULT NULL auto_increment PRIMARY KEY".freeze,
:string => { :name => "varchar", :limit => 255 },
:text => { :name => "text" },
:integer => { :name => "int", :limit => 4 },
:float => { :name => "float" },
:decimal => { :name => "decimal" },
:datetime => { :name => "datetime" },
:timestamp => { :name => "datetime" },
:time => { :name => "time" },
:date => { :name => "date" },
:binary => { :name => "blob" },
:boolean => { :name => "tinyint", :limit => 1 }
}
def initialize(connection, logger, connection_options, config)
super(connection, logger)
@ -187,7 +199,7 @@ module ActiveRecord
end
def adapter_name #:nodoc:
'MySQL'
ADAPTER_NAME
end
def supports_migrations? #:nodoc:
@ -195,20 +207,7 @@ module ActiveRecord
end
def native_database_types #:nodoc:
{
:primary_key => "int(11) DEFAULT NULL auto_increment PRIMARY KEY".freeze,
:string => { :name => "varchar", :limit => 255 },
:text => { :name => "text" },
:integer => { :name => "int", :limit => 4 },
:float => { :name => "float" },
:decimal => { :name => "decimal" },
:datetime => { :name => "datetime" },
:timestamp => { :name => "datetime" },
:time => { :name => "time" },
:date => { :name => "date" },
:binary => { :name => "blob" },
:boolean => { :name => "tinyint", :limit => 1 }
}
NATIVE_DATABASE_TYPES
end
@ -219,7 +218,7 @@ module ActiveRecord
s = column.class.string_to_binary(value).unpack("H*")[0]
"x'#{s}'"
elsif value.kind_of?(BigDecimal)
"'#{value.to_s("F")}'"
value.to_s("F")
else
super
end
@ -286,6 +285,14 @@ module ActiveRecord
@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 ======================================
@ -364,9 +371,9 @@ module ActiveRecord
end
end
def recreate_database(name) #:nodoc:
def recreate_database(name, options = {}) #:nodoc:
drop_database(name)
create_database(name)
create_database(name, options)
end
# Create a new MySQL database with optional <tt>:charset</tt> and <tt>:collation</tt>.
@ -517,14 +524,33 @@ module ActiveRecord
keys.length == 1 ? [keys.first, nil] : nil
end
def case_sensitive_equality_operator
"= BINARY"
end
def limited_update_conditions(where_sql, quoted_table_name, quoted_primary_key)
where_sql
end
private
def connect
@connection.reconnect = true if @connection.respond_to?(:reconnect=)
encoding = @config[:encoding]
if encoding
@connection.options(Mysql::SET_CHARSET_NAME, encoding) rescue nil
end
@connection.ssl_set(@config[:sslkey], @config[:sslcert], @config[:sslca], @config[:sslcapath], @config[:sslcipher]) if @config[:sslkey]
if @config[:sslca] || @config[:sslkey]
@connection.ssl_set(@config[:sslkey], @config[:sslcert], @config[:sslca], @config[:sslcapath], @config[:sslcipher])
end
@connection.real_connect(*@connection_options)
configure_connection
end
def configure_connection
encoding = @config[:encoding]
execute("SET NAMES '#{encoding}'") if encoding
# By default, MySQL 'where id is null' selects the last inserted id.

View file

@ -246,9 +246,26 @@ module ActiveRecord
# * <tt>:min_messages</tt> - An optional client min messages that is used in a <tt>SET client_min_messages TO <min_messages></tt> call on the connection.
# * <tt>:allow_concurrency</tt> - If true, use async query methods so Ruby threads don't deadlock; otherwise, use blocking query methods.
class PostgreSQLAdapter < AbstractAdapter
ADAPTER_NAME = 'PostgreSQL'.freeze
NATIVE_DATABASE_TYPES = {
:primary_key => "serial primary key".freeze,
:string => { :name => "character varying", :limit => 255 },
:text => { :name => "text" },
:integer => { :name => "integer" },
:float => { :name => "float" },
:decimal => { :name => "decimal" },
:datetime => { :name => "timestamp" },
:timestamp => { :name => "timestamp" },
:time => { :name => "time" },
:date => { :name => "date" },
:binary => { :name => "bytea" },
:boolean => { :name => "boolean" }
}
# Returns 'PostgreSQL' as adapter name for identification purposes.
def adapter_name
'PostgreSQL'
ADAPTER_NAME
end
# Initializes and connects a PostgreSQL adapter.
@ -290,20 +307,7 @@ module ActiveRecord
end
def native_database_types #:nodoc:
{
:primary_key => "serial primary key",
:string => { :name => "character varying", :limit => 255 },
:text => { :name => "text" },
:integer => { :name => "integer" },
:float => { :name => "float" },
:decimal => { :name => "decimal" },
:datetime => { :name => "timestamp" },
:timestamp => { :name => "timestamp" },
:time => { :name => "time" },
:date => { :name => "date" },
:binary => { :name => "bytea" },
:boolean => { :name => "boolean" }
}
NATIVE_DATABASE_TYPES
end
# Does PostgreSQL support migrations?
@ -331,6 +335,10 @@ module ActiveRecord
postgresql_version >= 80200
end
def supports_ddl_transactions?
true
end
# Returns the configured supported identifier length supported by PostgreSQL,
# or report the default of 63 on PostgreSQL 7.x.
def table_alias_length
@ -510,6 +518,45 @@ module ActiveRecord
execute "ROLLBACK"
end
# ruby-pg defines Ruby constants for transaction status,
# ruby-postgres does not.
PQTRANS_IDLE = defined?(PGconn::PQTRANS_IDLE) ? PGconn::PQTRANS_IDLE : 0
# Check whether a transaction is active.
def transaction_active?
@connection.transaction_status != PQTRANS_IDLE
end
# Wrap a block in a transaction. Returns result of block.
def transaction(start_db_transaction = true)
transaction_open = false
begin
if block_given?
if start_db_transaction
begin_db_transaction
transaction_open = true
end
yield
end
rescue Exception => database_transaction_rollback
if transaction_open && transaction_active?
transaction_open = false
rollback_db_transaction
end
raise unless database_transaction_rollback.is_a? ActiveRecord::Rollback
end
ensure
if transaction_open && transaction_active?
begin
commit_db_transaction
rescue Exception => database_transaction_rollback
rollback_db_transaction
raise
end
end
end
# SCHEMA STATEMENTS ========================================
def recreate_database(name) #:nodoc:
@ -618,6 +665,19 @@ module ActiveRecord
end
end
# Returns the current database name.
def current_database
query('select current_database()')[0][0]
end
# Returns the current database encoding format.
def encoding
query(<<-end_sql)[0][0]
SELECT pg_encoding_to_char(pg_database.encoding) FROM pg_database
WHERE pg_database.datname LIKE '#{current_database}'
end_sql
end
# Sets the schema search path to a string of comma-separated schema names.
# Names beginning with $ have to be quoted (e.g. $user => '$user').
# See: http://www.postgresql.org/docs/current/static/ddl-schemas.html
@ -851,7 +911,7 @@ module ActiveRecord
end
private
# The internal PostgreSQL identifer of the money data type.
# The internal PostgreSQL identifier of the money data type.
MONEY_COLUMN_TYPE_OID = 790 #:nodoc:
# Connects to a PostgreSQL server and sets up the adapter depending on the

View file

@ -34,8 +34,10 @@ module ActiveRecord
# person.name << 'by'
# person.name_change # => ['uncle bob', 'uncle bobby']
module Dirty
DIRTY_SUFFIXES = ['_changed?', '_change', '_will_change!', '_was']
def self.included(base)
base.attribute_method_suffix '_changed?', '_change', '_will_change!', '_was'
base.attribute_method_suffix *DIRTY_SUFFIXES
base.alias_method_chain :write_attribute, :dirty
base.alias_method_chain :save, :dirty
base.alias_method_chain :save!, :dirty
@ -44,6 +46,8 @@ module ActiveRecord
base.superclass_delegating_accessor :partial_updates
base.partial_updates = true
base.send(:extend, ClassMethods)
end
# Do any attributes have unsaved changes?
@ -62,7 +66,7 @@ module ActiveRecord
changed_attributes.keys
end
# Map of changed attrs => [original value, new value]
# Map of changed attrs => [original value, new value].
# person.changes # => {}
# person.name = 'bob'
# person.changes # => { 'name' => ['bill', 'bob'] }
@ -93,27 +97,27 @@ module ActiveRecord
end
private
# Map of change attr => original value.
# Map of change <tt>attr => original value</tt>.
def changed_attributes
@changed_attributes ||= {}
end
# Handle *_changed? for method_missing.
# Handle <tt>*_changed?</tt> for +method_missing+.
def attribute_changed?(attr)
changed_attributes.include?(attr)
end
# Handle *_change for method_missing.
# Handle <tt>*_change</tt> for +method_missing+.
def attribute_change(attr)
[changed_attributes[attr], __send__(attr)] if attribute_changed?(attr)
end
# Handle *_was for method_missing.
# Handle <tt>*_was</tt> for +method_missing+.
def attribute_was(attr)
attribute_changed?(attr) ? changed_attributes[attr] : __send__(attr)
end
# Handle *_will_change! for method_missing.
# Handle <tt>*_will_change!</tt> for +method_missing+.
def attribute_will_change!(attr)
changed_attributes[attr] = clone_attribute_value(:read_attribute, attr)
end
@ -161,5 +165,19 @@ module ActiveRecord
old != value
end
module ClassMethods
def self.extended(base)
base.metaclass.alias_method_chain(:alias_attribute, :dirty)
end
def alias_attribute_with_dirty(new_name, old_name)
alias_attribute_without_dirty(new_name, old_name)
DIRTY_SUFFIXES.each do |suffix|
module_eval <<-STR, __FILE__, __LINE__+1
def #{new_name}#{suffix}; self.#{old_name}#{suffix}; end
STR
end
end
end
end
end

View file

@ -0,0 +1,41 @@
module ActiveRecord
class DynamicFinderMatch
def self.match(method)
df_match = self.new(method)
df_match.finder ? df_match : nil
end
def initialize(method)
@finder = :first
case method.to_s
when /^find_(all_by|last_by|by)_([_a-zA-Z]\w*)$/
@finder = :last if $1 == 'last_by'
@finder = :all if $1 == 'all_by'
names = $2
when /^find_by_([_a-zA-Z]\w*)\!$/
@bang = true
names = $1
when /^find_or_(initialize|create)_by_([_a-zA-Z]\w*)$/
@instantiator = $1 == 'initialize' ? :new : :create
names = $2
else
@finder = nil
end
@attribute_names = names && names.split('_and_')
end
attr_reader :finder, :attribute_names, :instantiator
def finder?
!@finder.nil? && @instantiator.nil?
end
def instantiator?
@finder == :first && !@instantiator.nil?
end
def bang?
@bang
end
end
end

View file

@ -515,7 +515,7 @@ class Fixtures < (RUBY_VERSION < '1.9' ? YAML::Omap : Hash)
all_loaded_fixtures.update(fixtures_map)
connection.transaction(Thread.current['open_transactions'].to_i == 0) do
connection.transaction(connection.open_transactions.zero?) do
fixtures.reverse.each { |fixture| fixture.delete_existing_fixtures }
fixtures.each { |fixture| fixture.insert_fixtures }
@ -541,13 +541,14 @@ class Fixtures < (RUBY_VERSION < '1.9' ? YAML::Omap : Hash)
label.to_s.hash.abs
end
attr_reader :table_name
attr_reader :table_name, :name
def initialize(connection, table_name, class_name, fixture_path, file_filter = DEFAULT_FILTER_RE)
@connection, @table_name, @fixture_path, @file_filter = connection, table_name, fixture_path, file_filter
@name = table_name # preserve fixture base name
@class_name = class_name ||
(ActiveRecord::Base.pluralize_table_names ? @table_name.singularize.camelize : @table_name.camelize)
@table_name = ActiveRecord::Base.table_name_prefix + @table_name + ActiveRecord::Base.table_name_suffix
@table_name = "#{ActiveRecord::Base.table_name_prefix}#{@table_name}#{ActiveRecord::Base.table_name_suffix}"
@table_name = class_name.table_name if class_name.respond_to?(:table_name)
@connection = class_name.connection if class_name.respond_to?(:connection)
read_fixture_files
@ -929,7 +930,7 @@ module Test #:nodoc:
load_fixtures
@@already_loaded_fixtures[self.class] = @loaded_fixtures
end
ActiveRecord::Base.send :increment_open_transactions
ActiveRecord::Base.connection.increment_open_transactions
ActiveRecord::Base.connection.begin_db_transaction
# Load fixtures for every test.
else
@ -950,11 +951,11 @@ module Test #:nodoc:
end
# Rollback changes if a transaction is active.
if use_transactional_fixtures? && Thread.current['open_transactions'] != 0
if use_transactional_fixtures? && ActiveRecord::Base.connection.open_transactions != 0
ActiveRecord::Base.connection.rollback_db_transaction
Thread.current['open_transactions'] = 0
ActiveRecord::Base.connection.decrement_open_transactions
end
ActiveRecord::Base.verify_active_connections!
ActiveRecord::Base.clear_active_connections!
end
private
@ -963,9 +964,9 @@ module Test #:nodoc:
fixtures = Fixtures.create_fixtures(fixture_path, fixture_table_names, fixture_class_names)
unless fixtures.nil?
if fixtures.instance_of?(Fixtures)
@loaded_fixtures[fixtures.table_name] = fixtures
@loaded_fixtures[fixtures.name] = fixtures
else
fixtures.each { |f| @loaded_fixtures[f.table_name] = f }
fixtures.each { |f| @loaded_fixtures[f.name] = f }
end
end
end

View file

@ -0,0 +1,26 @@
# Deprecates the use of the former message interpolation syntax in activerecord
# as in "must have %d characters". The new syntax uses explicit variable names
# as in "{{value}} must have {{count}} characters".
require 'i18n/backend/simple'
module I18n
module Backend
class Simple
DEPRECATED_INTERPOLATORS = { '%d' => '{{count}}', '%s' => '{{value}}' }
protected
def interpolate_with_deprecated_syntax(locale, string, values = {})
return string unless string.is_a?(String)
string = string.gsub(/%d|%s/) do |s|
instead = DEPRECATED_INTERPOLATORS[s]
ActiveSupport::Deprecation.warn "using #{s} in messages is deprecated; use #{instead} instead."
instead
end
interpolate_without_deprecated_syntax(locale, string, values)
end
alias_method_chain :interpolate, :deprecated_syntax
end
end
end

View file

@ -0,0 +1,54 @@
en-US:
activerecord:
errors:
# The values :model, :attribute and :value are always available for interpolation
# The value :count is available when applicable. Can be used for pluralization.
messages:
inclusion: "is not included in the list"
exclusion: "is reserved"
invalid: "is invalid"
confirmation: "doesn't match confirmation"
accepted: "must be accepted"
empty: "can't be empty"
blank: "can't be blank"
too_long: "is too long (maximum is {{count}} characters)"
too_short: "is too short (minimum is {{count}} characters)"
wrong_length: "is the wrong length (should be {{count}} characters)"
taken: "has already been taken"
not_a_number: "is not a number"
greater_than: "must be greater than {{count}}"
greater_than_or_equal_to: "must be greater than or equal to {{count}}"
equal_to: "must be equal to {{count}}"
less_than: "must be less than {{count}}"
less_than_or_equal_to: "must be less than or equal to {{count}}"
odd: "must be odd"
even: "must be even"
# Append your own errors here or at the model/attributes scope.
# You can define own errors for models or model attributes.
# The values :model, :attribute and :value are always available for interpolation.
#
# For example,
# models:
# user:
# blank: "This is a custom blank message for {{model}}: {{attribute}}"
# attributes:
# login:
# blank: "This is a custom blank message for User login"
# Will define custom blank validation message for User model and
# custom blank validation message for login attribute of User model.
models:
# Translate model names. Used in Model.human_name().
#models:
# For example,
# user: "Dude"
# will translate User model name to "Dude"
# Translate model attribute names. Used in Model.human_attribute_name(attribute).
#attributes:
# For example,
# user:
# login: "Handle"
# will translate User attribute "login" as "Handle"

View file

@ -349,6 +349,27 @@ module ActiveRecord
end
end
# MigrationProxy is used to defer loading of the actual migration classes
# until they are needed
class MigrationProxy
attr_accessor :name, :version, :filename
delegate :migrate, :announce, :write, :to=>:migration
private
def migration
@migration ||= load_migration
end
def load_migration
load(filename)
name.constantize
end
end
class Migrator#:nodoc:
class << self
def migrate(migrations_path, target_version = nil)
@ -443,17 +464,25 @@ module ActiveRecord
runnable.pop if down? && !target.nil?
runnable.each do |migration|
Base.logger.info "Migrating to #{migration} (#{migration.version})"
Base.logger.info "Migrating to #{migration.name} (#{migration.version})"
# On our way up, we skip migrating the ones we've already migrated
# On our way down, we skip reverting the ones we've never migrated
next if up? && migrated.include?(migration.version.to_i)
# On our way down, we skip reverting the ones we've never migrated
if down? && !migrated.include?(migration.version.to_i)
migration.announce 'never migrated, skipping'; migration.write
else
migration.migrate(@direction)
record_version_state_after_migrating(migration.version)
next
end
begin
ddl_transaction do
migration.migrate(@direction)
record_version_state_after_migrating(migration.version)
end
rescue => e
canceled_msg = Base.connection.supports_ddl_transactions? ? "this and " : ""
raise StandardError, "An error has occurred, #{canceled_msg}all later migrations canceled:\n\n#{e}", e.backtrace
end
end
end
@ -476,11 +505,10 @@ module ActiveRecord
raise DuplicateMigrationNameError.new(name.camelize)
end
load(file)
klasses << returning(name.camelize.constantize) do |klass|
class << klass; attr_accessor :version end
klass.version = version
klasses << returning(MigrationProxy.new) do |migration|
migration.name = name.camelize
migration.version = version
migration.filename = file
end
end
@ -519,5 +547,14 @@ module ActiveRecord
def down?
@direction == :down
end
# Wrap the migration in a transaction only if supported by the adapter.
def ddl_transaction(&block)
if Base.connection.supports_ddl_transactions?
Base.transaction { block.call }
else
block.call
end
end
end
end

View file

@ -1,10 +1,10 @@
module ActiveRecord
module NamedScope
# All subclasses of ActiveRecord::Base have two named_scopes:
# * <tt>all</tt>, which is similar to a <tt>find(:all)</tt> query, and
# * <tt>scoped</tt>, which allows for the creation of anonymous scopes, on the fly: <tt>Shirt.scoped(:conditions => {:color => 'red'}).scoped(:include => :washing_instructions)</tt>
# All subclasses of ActiveRecord::Base have two named \scopes:
# * <tt>all</tt> - which is similar to a <tt>find(:all)</tt> query, and
# * <tt>scoped</tt> - which allows for the creation of anonymous \scopes, on the fly: <tt>Shirt.scoped(:conditions => {:color => 'red'}).scoped(:include => :washing_instructions)</tt>
#
# These anonymous scopes tend to be useful when procedurally generating complex queries, where passing
# These anonymous \scopes tend to be useful when procedurally generating complex queries, where passing
# intermediate values (scopes) around as first-class objects is convenient.
def self.included(base)
base.class_eval do
@ -26,20 +26,20 @@ module ActiveRecord
# named_scope :dry_clean_only, :joins => :washing_instructions, :conditions => ['washing_instructions.dry_clean_only = ?', true]
# end
#
# The above calls to <tt>named_scope</tt> define class methods <tt>Shirt.red</tt> and <tt>Shirt.dry_clean_only</tt>. <tt>Shirt.red</tt>,
# The above calls to <tt>named_scope</tt> define class methods Shirt.red and Shirt.dry_clean_only. Shirt.red,
# in effect, represents the query <tt>Shirt.find(:all, :conditions => {:color => 'red'})</tt>.
#
# Unlike Shirt.find(...), however, the object returned by <tt>Shirt.red</tt> is not an Array; it resembles the association object
# Unlike <tt>Shirt.find(...)</tt>, however, the object returned by Shirt.red is not an Array; it resembles the association object
# constructed by a <tt>has_many</tt> declaration. For instance, you can invoke <tt>Shirt.red.find(:first)</tt>, <tt>Shirt.red.count</tt>,
# <tt>Shirt.red.find(:all, :conditions => {:size => 'small'})</tt>. Also, just
# as with the association objects, name scopes acts like an Array, implementing Enumerable; <tt>Shirt.red.each(&block)</tt>,
# <tt>Shirt.red.first</tt>, and <tt>Shirt.red.inject(memo, &block)</tt> all behave as if Shirt.red really were an Array.
# as with the association objects, named \scopes act like an Array, implementing Enumerable; <tt>Shirt.red.each(&block)</tt>,
# <tt>Shirt.red.first</tt>, and <tt>Shirt.red.inject(memo, &block)</tt> all behave as if Shirt.red really was an Array.
#
# These named scopes are composable. For instance, <tt>Shirt.red.dry_clean_only</tt> will produce all shirts that are both red and dry clean only.
# These named \scopes are composable. For instance, <tt>Shirt.red.dry_clean_only</tt> will produce all shirts that are both red and dry clean only.
# Nested finds and calculations also work with these compositions: <tt>Shirt.red.dry_clean_only.count</tt> returns the number of garments
# for which these criteria obtain. Similarly with <tt>Shirt.red.dry_clean_only.average(:thread_count)</tt>.
#
# All scopes are available as class methods on the ActiveRecord::Base descendent upon which the scopes were defined. But they are also available to
# All \scopes are available as class methods on the ActiveRecord::Base descendent upon which the \scopes were defined. But they are also available to
# <tt>has_many</tt> associations. If,
#
# class Person < ActiveRecord::Base
@ -49,7 +49,7 @@ 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 be procedural.
# Named \scopes can also be procedural:
#
# class Shirt < ActiveRecord::Base
# named_scope :colored, lambda { |color|
@ -59,7 +59,7 @@ module ActiveRecord
#
# In this example, <tt>Shirt.colored('puce')</tt> finds all puce shirts.
#
# Named scopes can also have extensions, just as with <tt>has_many</tt> declarations:
# Named \scopes can also have extensions, just as with <tt>has_many</tt> declarations:
#
# class Shirt < ActiveRecord::Base
# named_scope :red, :conditions => {:color => 'red'} do
@ -70,7 +70,7 @@ module ActiveRecord
# end
#
#
# For testing complex named scopes, you can examine the scoping options using the
# For testing complex named \scopes, you can examine the scoping options using the
# <tt>proxy_options</tt> method on the proxy itself.
#
# class Shirt < ActiveRecord::Base
@ -101,9 +101,9 @@ module ActiveRecord
class Scope
attr_reader :proxy_scope, :proxy_options
NON_DELEGATE_METHODS = %w(nil? send object_id class extend find size count sum average maximum minimum paginate first last empty? any? respond_to?).to_set
[].methods.each do |m|
unless m =~ /(^__|^nil\?|^send|^object_id$|class|extend|^find$|count|sum|average|maximum|minimum|paginate|first|last|empty\?|respond_to\?)/
unless m =~ /^__/ || NON_DELEGATE_METHODS.include?(m.to_s)
delegate m, :to => :proxy_found
end
end
@ -136,6 +136,10 @@ module ActiveRecord
end
end
def size
@found ? @found.length : count
end
def empty?
@found ? @found.empty? : count.zero?
end
@ -144,6 +148,14 @@ module ActiveRecord
super || @proxy_scope.respond_to?(method, include_private)
end
def any?
if block_given?
proxy_found.any? { |*block_args| yield(*block_args) }
else
!empty?
end
end
protected
def proxy_found
@found || load_found
@ -154,7 +166,8 @@ module ActiveRecord
if scopes.include?(method)
scopes[method].call(self, *args)
else
with_scope :find => proxy_options do
with_scope :find => proxy_options, :create => proxy_options[:conditions].is_a?(Hash) ? proxy_options[:conditions] : {} do
method = :new if method == :build
proxy_scope.send(method, *args, &block)
end
end

View file

@ -13,14 +13,15 @@ module ActiveRecord
def create_reflection(macro, name, options, active_record)
case macro
when :has_many, :belongs_to, :has_one, :has_and_belongs_to_many
reflection = AssociationReflection.new(macro, name, options, active_record)
klass = options[:through] ? ThroughReflection : AssociationReflection
reflection = klass.new(macro, name, options, active_record)
when :composed_of
reflection = AggregateReflection.new(macro, name, options, active_record)
end
write_inheritable_hash :reflections, name => reflection
reflection
end
# Returns a hash containing all AssociationReflection objects for the current class
# Example:
#
@ -30,7 +31,7 @@ module ActiveRecord
def reflections
read_inheritable_attribute(:reflections) || write_inheritable_attribute(:reflections, {})
end
# Returns an array of AggregateReflection objects for all the aggregations in the class.
def reflect_on_all_aggregations
reflections.values.select { |reflection| reflection.is_a?(AggregateReflection) }
@ -109,7 +110,16 @@ module ActiveRecord
# Returns +true+ if +self+ and +other_aggregation+ have the same +name+ attribute, +active_record+ attribute,
# and +other_aggregation+ has an options hash assigned to it.
def ==(other_aggregation)
name == other_aggregation.name && other_aggregation.options && active_record == other_aggregation.active_record
other_aggregation.kind_of?(self.class) && name == other_aggregation.name && other_aggregation.options && active_record == other_aggregation.active_record
end
def sanitized_conditions #:nodoc:
@sanitized_conditions ||= klass.send(:sanitize_sql, options[:conditions]) if options[:conditions]
end
# Returns +true+ if +self+ is a +belongs_to+ reflection.
def belongs_to?
macro == :belongs_to
end
private
@ -125,10 +135,45 @@ module ActiveRecord
# Holds all the meta-data about an association as it was specified in the Active Record class.
class AssociationReflection < MacroReflection #:nodoc:
# Returns the target association's class:
#
# class Author < ActiveRecord::Base
# has_many :books
# end
#
# Author.reflect_on_association(:books).klass
# # => Book
#
# <b>Note:</b> do not call +klass.new+ or +klass.create+ to instantiate
# a new association object. Use +build_association+ or +create_association+
# instead. This allows plugins to hook into association object creation.
def klass
@klass ||= active_record.send(:compute_type, class_name)
end
# Returns a new, unsaved instance of the associated class. +options+ will
# be passed to the class's constructor.
def build_association(*options)
klass.new(*options)
end
# Creates a new instance of the associated class, and immediates saves it
# with ActiveRecord::Base#save. +options+ will be passed to the class's
# creation method. Returns the newly created object.
def create_association(*options)
klass.create(*options)
end
# Creates a new instance of the associated class, and immediates saves it
# with ActiveRecord::Base#save!. +options+ will be passed to the class's
# creation method. If the created record doesn't pass validations, then an
# exception will be raised.
#
# Returns the newly created object.
def create_association!(*options)
klass.create!(*options)
end
def table_name
@table_name ||= klass.table_name
end
@ -153,6 +198,52 @@ module ActiveRecord
end
end
def check_validity!
end
def through_reflection
false
end
def through_reflection_primary_key_name
end
def source_reflection
nil
end
private
def derive_class_name
class_name = name.to_s.camelize
class_name = class_name.singularize if [ :has_many, :has_and_belongs_to_many ].include?(macro)
class_name
end
def derive_primary_key_name
if belongs_to?
"#{name}_id"
elsif options[:as]
"#{options[:as]}_id"
else
active_record.name.foreign_key
end
end
end
# Holds all the meta-data about a :through association as it was specified in the Active Record class.
class ThroughReflection < AssociationReflection #:nodoc:
# Gets the source of the through reflection. It checks both a singularized and pluralized form for <tt>:belongs_to</tt> or <tt>:has_many</tt>.
# (The <tt>:tags</tt> association on Tagging below.)
#
# class Post < ActiveRecord::Base
# has_many :taggings
# has_many :tags, :through => :taggings
# end
#
def source_reflection
@source_reflection ||= source_reflection_names.collect { |name| through_reflection.klass.reflect_on_association(name) }.compact.first
end
# Returns the AssociationReflection object specified in the <tt>:through</tt> option
# of a HasManyThrough or HasOneThrough association. Example:
#
@ -165,7 +256,7 @@ module ActiveRecord
# taggings_reflection = tags_reflection.through_reflection
#
def through_reflection
@through_reflection ||= options[:through] ? active_record.reflect_on_association(options[:through]) : false
@through_reflection ||= active_record.reflect_on_association(options[:through])
end
# Gets an array of possible <tt>:through</tt> source reflection names:
@ -176,63 +267,40 @@ module ActiveRecord
@source_reflection_names ||= (options[:source] ? [options[:source]] : [name.to_s.singularize, name]).collect { |n| n.to_sym }
end
# Gets the source of the through reflection. It checks both a singularized and pluralized form for <tt>:belongs_to</tt> or <tt>:has_many</tt>.
# (The <tt>:tags</tt> association on Tagging below.)
#
# class Post < ActiveRecord::Base
# has_many :taggings
# has_many :tags, :through => :taggings
# end
#
def source_reflection
return nil unless through_reflection
@source_reflection ||= source_reflection_names.collect { |name| through_reflection.klass.reflect_on_association(name) }.compact.first
def check_validity!
if through_reflection.nil?
raise HasManyThroughAssociationNotFoundError.new(active_record.name, self)
end
if source_reflection.nil?
raise HasManyThroughSourceAssociationNotFoundError.new(self)
end
if options[:source_type] && source_reflection.options[:polymorphic].nil?
raise HasManyThroughAssociationPointlessSourceTypeError.new(active_record.name, self, source_reflection)
end
if source_reflection.options[:polymorphic] && options[:source_type].nil?
raise HasManyThroughAssociationPolymorphicError.new(active_record.name, self, source_reflection)
end
unless [:belongs_to, :has_many].include?(source_reflection.macro) && source_reflection.options[:through].nil?
raise HasManyThroughSourceAssociationMacroError.new(self)
end
end
def check_validity!
if options[:through]
if through_reflection.nil?
raise HasManyThroughAssociationNotFoundError.new(active_record.name, self)
end
if source_reflection.nil?
raise HasManyThroughSourceAssociationNotFoundError.new(self)
end
def through_reflection_primary_key
through_reflection.belongs_to? ? through_reflection.klass.primary_key : through_reflection.primary_key_name
end
if options[:source_type] && source_reflection.options[:polymorphic].nil?
raise HasManyThroughAssociationPointlessSourceTypeError.new(active_record.name, self, source_reflection)
end
if source_reflection.options[:polymorphic] && options[:source_type].nil?
raise HasManyThroughAssociationPolymorphicError.new(active_record.name, self, source_reflection)
end
unless [:belongs_to, :has_many].include?(source_reflection.macro) && source_reflection.options[:through].nil?
raise HasManyThroughSourceAssociationMacroError.new(self)
end
end
def through_reflection_primary_key_name
through_reflection.primary_key_name if through_reflection.belongs_to?
end
private
def derive_class_name
# get the class_name of the belongs_to association of the through reflection
if through_reflection
options[:source_type] || source_reflection.class_name
else
class_name = name.to_s.camelize
class_name = class_name.singularize if [ :has_many, :has_and_belongs_to_many ].include?(macro)
class_name
end
end
def derive_primary_key_name
if macro == :belongs_to
"#{name}_id"
elsif options[:as]
"#{options[:as]}_id"
else
active_record.name.foreign_key
end
options[:source_type] || source_reflection.class_name
end
end
end

View file

@ -102,7 +102,7 @@ HEADER
spec[:precision] = column.precision.inspect if !column.precision.nil?
spec[:scale] = column.scale.inspect if !column.scale.nil?
spec[:null] = 'false' if !column.null
spec[:default] = default_string(column.default) if !column.default.nil?
spec[:default] = default_string(column.default) if column.has_default?
(spec.keys - [:name, :type]).each{ |k| spec[k].insert(0, "#{k.inspect} => ")}
spec
end.compact

View file

@ -37,11 +37,26 @@ module ActiveRecord
$queries_executed = []
yield
ensure
assert_equal num, $queries_executed.size, "#{$queries_executed.size} instead of #{num} queries were executed."
assert_equal num, $queries_executed.size, "#{$queries_executed.size} instead of #{num} queries were executed.#{$queries_executed.size == 0 ? '' : "\nQueries:\n#{$queries_executed.join("\n")}"}"
end
def assert_no_queries(&block)
assert_queries(0, &block)
end
def self.use_concurrent_connections
setup :connection_allow_concurrency_setup
teardown :connection_allow_concurrency_teardown
end
def connection_allow_concurrency_setup
@connection = ActiveRecord::Base.remove_connection
ActiveRecord::Base.establish_connection(@connection.merge({:allow_concurrency => true}))
end
def connection_allow_concurrency_teardown
ActiveRecord::Base.clear_all_connections!
ActiveRecord::Base.establish_connection(@connection)
end
end
end

View file

@ -1,7 +1,8 @@
require 'thread'
module ActiveRecord
module Transactions # :nodoc:
# See ActiveRecord::Transactions::ClassMethods for documentation.
module Transactions
class TransactionError < ActiveRecordError # :nodoc:
end
@ -15,26 +16,33 @@ module ActiveRecord
end
end
# 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 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. Example:
# 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
# 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.
# Example:
#
# transaction do
# ActiveRecord::Base.transaction do
# david.withdrawal(100)
# mary.deposit(100)
# end
#
# This example will only take money from David and give to Mary if neither +withdrawal+ nor +deposit+ raises an exception.
# Exceptions will force a ROLLBACK that returns the database to the state before the transaction was begun. Be aware, though,
# that the objects will _not_ have their instance data returned to their pre-transactional state.
# This example will only take money from David and give to Mary if neither
# +withdrawal+ nor +deposit+ raises an exception. Exceptions will force a
# ROLLBACK that returns the database to the state before the transaction was
# begun. Be aware, though, that the objects will _not_ have their instance
# data returned to their pre-transactional state.
#
# == Different Active Record classes in a single transaction
#
# 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.
# that class. This is because transactions are per-database connection, not
# per-model.
#
# In this example a <tt>Balance</tt> record is transactionally saved even
# though <tt>transaction</tt> is called on the <tt>Account</tt> class:
#
@ -43,6 +51,14 @@ module ActiveRecord
# account.save!
# end
#
# Note that the +transaction+ method is also available as a model instance
# method. For example, you can also do this:
#
# balance.transaction do
# balance.save!
# account.save!
# end
#
# == Transactions are not distributed across database connections
#
# A transaction acts on a single database connection. If you have
@ -62,48 +78,72 @@ module ActiveRecord
#
# == Save and destroy are automatically wrapped in a transaction
#
# Both Base#save and Base#destroy come wrapped in a transaction that ensures that whatever you do in validations or callbacks
# will happen under the protected cover of a transaction. So you can use validations to check for values that the transaction
# depends on or you can raise exceptions in the callbacks to rollback.
# Both Base#save and Base#destroy come wrapped in a transaction that ensures
# that whatever you do in validations or callbacks will happen under the
# protected cover of a transaction. So you can use validations to check for
# values that the transaction depends on or you can raise exceptions in the
# callbacks to rollback, including <tt>after_*</tt> callbacks.
#
# == Exception handling
# == Exception handling and rolling back
#
# Also have in mind that exceptions thrown within a transaction block will be propagated (after triggering the ROLLBACK), so you
# should be ready to catch those in your application code. One exception is the ActiveRecord::Rollback exception, which will
# trigger a ROLLBACK when raised, but not be re-raised by the transaction block.
# Also have in mind that exceptions thrown within a transaction block will
# be propagated (after triggering the ROLLBACK), so you should be ready to
# catch those in your application code.
#
# 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 ActiveRecord::StatementInvalid exceptions
# inside a transaction block. 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 causes the entire transaction to become unusable
# until it's restarted from the beginning. Here is an example which
# demonstrates the problem:
#
# # Suppose that we have a Number model with a unique column called 'i'.
# Number.transaction do
# Number.create(:i => 0)
# begin
# # This will raise a unique constraint error...
# Number.create(:i => 0)
# rescue ActiveRecord::StatementInvalid
# # ...which we ignore.
# end
#
# # On PostgreSQL, the transaction is now unusable. The following
# # statement will cause a PostgreSQL error, even though the unique
# # constraint is no longer violated:
# Number.create(:i => 1)
# # => "PGError: ERROR: current transaction is aborted, commands
# # ignored until end of transaction block"
# end
#
# One should restart the entire transaction if a StatementError occurred.
module ClassMethods
# See ActiveRecord::Transactions::ClassMethods for detailed documentation.
def transaction(&block)
increment_open_transactions
connection.increment_open_transactions
begin
connection.transaction(Thread.current['start_db_transaction'], &block)
connection.transaction(connection.open_transactions == 1, &block)
ensure
decrement_open_transactions
connection.decrement_open_transactions
end
end
private
def increment_open_transactions #:nodoc:
open = Thread.current['open_transactions'] ||= 0
Thread.current['start_db_transaction'] = open.zero?
Thread.current['open_transactions'] = open + 1
end
def decrement_open_transactions #:nodoc:
Thread.current['open_transactions'] -= 1
end
end
# See ActiveRecord::Transactions::ClassMethods for detailed documentation.
def transaction(&block)
self.class.transaction(&block)
end
def destroy_with_transactions #:nodoc:
transaction { destroy_without_transactions }
with_transaction_returning_status(:destroy_without_transactions)
end
def save_with_transactions(perform_validation = true) #:nodoc:
rollback_active_record_state! { transaction { save_without_transactions(perform_validation) } }
rollback_active_record_state! { with_transaction_returning_status(:save_without_transactions, perform_validation) }
end
def save_with_transactions! #:nodoc:
@ -126,5 +166,20 @@ module ActiveRecord
end
raise
end
# Executes +method+ within a transaction and captures its return value as a
# status flag. If the status is true the transaction is committed, otherwise
# a ROLLBACK is issued. In any case the status flag is returned.
#
# This method is available within the context of an ActiveRecord::Base
# instance.
def with_transaction_returning_status(method, *args)
status = nil
transaction do
status = send(method, *args)
raise ActiveRecord::Rollback unless status
end
status
end
end
end

View file

@ -1,5 +1,5 @@
module ActiveRecord
# Raised by save! and create! when the record is invalid. Use the
# 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.
# begin
# complex_operation_that_calls_save!_internally
@ -18,70 +18,97 @@ module ActiveRecord
# determine whether the object is in a valid state to be saved. See usage example in Validations.
class Errors
include Enumerable
class << self
def default_error_messages
ActiveSupport::Deprecation.warn("ActiveRecord::Errors.default_error_messages has been deprecated. Please use I18n.translate('activerecord.errors.messages').")
I18n.translate 'activerecord.errors.messages'
end
end
def initialize(base) # :nodoc:
@base, @errors = base, {}
end
@@default_error_messages = {
:inclusion => "is not included in the list",
:exclusion => "is reserved",
:invalid => "is invalid",
:confirmation => "doesn't match confirmation",
:accepted => "must be accepted",
:empty => "can't be empty",
:blank => "can't be blank",
:too_long => "is too long (maximum is %d characters)",
:too_short => "is too short (minimum is %d characters)",
:wrong_length => "is the wrong length (should be %d characters)",
:taken => "has already been taken",
:not_a_number => "is not a number",
:greater_than => "must be greater than %d",
:greater_than_or_equal_to => "must be greater than or equal to %d",
:equal_to => "must be equal to %d",
:less_than => "must be less than %d",
:less_than_or_equal_to => "must be less than or equal to %d",
:odd => "must be odd",
:even => "must be even"
}
# Holds a hash with all the default error messages that can be replaced by your own copy or localizations.
cattr_accessor :default_error_messages
# Adds an error to the base object instead of any particular attribute. This is used
# to report errors that don't tie to any specific attribute, but rather to the object
# as a whole. These error messages don't get prepended with any field name when iterating
# with each_full, so they should be complete sentences.
# with +each_full+, so they should be complete sentences.
def add_to_base(msg)
add(:base, msg)
end
# Adds an error message (+msg+) to the +attribute+, which will be returned on a call to <tt>on(attribute)</tt>
# Adds an error message (+messsage+) to the +attribute+, which will be returned on a call to <tt>on(attribute)</tt>
# for the same attribute and ensure that this error object returns false when asked if <tt>empty?</tt>. More than one
# error can be added to the same +attribute+ in which case an array will be returned on a call to <tt>on(attribute)</tt>.
# If no +msg+ is supplied, "invalid" is assumed.
def add(attribute, msg = @@default_error_messages[:invalid])
@errors[attribute.to_s] = [] if @errors[attribute.to_s].nil?
@errors[attribute.to_s] << msg
# If no +messsage+ is supplied, :invalid is assumed.
# If +message+ is a Symbol, it will be translated, using the appropriate scope (see translate_error).
def add(attribute, message = nil, options = {})
message ||= :invalid
message = generate_message(attribute, message, options) if message.is_a?(Symbol)
@errors[attribute.to_s] ||= []
@errors[attribute.to_s] << message
end
# Will add an error message to each of the attributes in +attributes+ that is empty.
def add_on_empty(attributes, msg = @@default_error_messages[:empty])
def add_on_empty(attributes, custom_message = nil)
for attr in [attributes].flatten
value = @base.respond_to?(attr.to_s) ? @base.send(attr.to_s) : @base[attr.to_s]
is_empty = value.respond_to?("empty?") ? value.empty? : false
add(attr, msg) unless !value.nil? && !is_empty
is_empty = value.respond_to?(:empty?) ? value.empty? : false
add(attr, :empty, :default => custom_message) unless !value.nil? && !is_empty
end
end
# Will add an error message to each of the attributes in +attributes+ that is blank (using Object#blank?).
def add_on_blank(attributes, msg = @@default_error_messages[:blank])
def add_on_blank(attributes, custom_message = nil)
for attr in [attributes].flatten
value = @base.respond_to?(attr.to_s) ? @base.send(attr.to_s) : @base[attr.to_s]
add(attr, msg) if value.blank?
add(attr, :blank, :default => custom_message) if value.blank?
end
end
# Translates an error message in it's default scope (<tt>activerecord.errrors.messages</tt>).
# Error messages are first looked up in <tt>models.MODEL.attributes.ATTRIBUTE.MESSAGE</tt>, if it's not there,
# it's looked up in <tt>models.MODEL.MESSAGE</tt> and if that is not there it returns the translation of the
# default message (e.g. <tt>activerecord.errors.messages.MESSAGE</tt>). The translated model name,
# translated attribute name and the value are available for interpolation.
#
# When using inheritence in your models, it will check all the inherited models too, but only if the model itself
# hasn't been found. Say you have <tt>class Admin < User; end</tt> and you wanted the translation for the <tt>:blank</tt>
# error +message+ for the <tt>title</tt> +attribute+, it looks for these translations:
#
# <ol>
# <li><tt>activerecord.errors.models.admin.attributes.title.blank</tt></li>
# <li><tt>activerecord.errors.models.admin.blank</tt></li>
# <li><tt>activerecord.errors.models.user.attributes.title.blank</tt></li>
# <li><tt>activerecord.errors.models.user.blank</tt></li>
# <li><tt>activerecord.errors.messages.blank</tt></li>
# <li>any default you provided through the +options+ hash (in the activerecord.errors scope)</li>
# </ol>
def generate_message(attribute, message = :invalid, options = {})
message, options[:default] = options[:default], message if options[:default].is_a?(Symbol)
defaults = @base.class.self_and_descendents_from_active_record.map do |klass|
[ :"models.#{klass.name.underscore}.attributes.#{attribute}.#{message}",
:"models.#{klass.name.underscore}.#{message}" ]
end
defaults << options.delete(:default)
defaults = defaults.compact.flatten << :"messages.#{message}"
key = defaults.shift
value = @base.respond_to?(attribute) ? @base.send(attribute) : nil
options = { :default => defaults,
:model => @base.class.human_name,
:attribute => @base.class.human_attribute_name(attribute.to_s),
:value => value,
:scope => [:activerecord, :errors]
}.merge(options)
I18n.translate(key, options)
end
# Returns true if the specified +attribute+ has errors associated with it.
#
@ -97,7 +124,7 @@ module ActiveRecord
!@errors[attribute.to_s].nil?
end
# Returns nil, if no errors are associated with the specified +attribute+.
# Returns +nil+, if no errors are associated with the specified +attribute+.
# Returns the error message, if one error is associated with the specified +attribute+.
# Returns an array of error messages, if more than one error is associated with the specified +attribute+.
#
@ -118,7 +145,7 @@ module ActiveRecord
alias :[] :on
# Returns errors assigned to the base object through add_to_base according to the normal rules of on(attribute).
# Returns errors assigned to the base object through +add_to_base+ according to the normal rules of <tt>on(attribute)</tt>.
def on_base
on(:base)
end
@ -131,15 +158,15 @@ module ActiveRecord
# end
#
# company = Company.create(:address => '123 First St.')
# company.errors.each{|attr,msg| puts "#{attr} - #{msg}" } # =>
# name - is too short (minimum is 5 characters)
# name - can't be blank
# address - can't be blank
# company.errors.each{|attr,msg| puts "#{attr} - #{msg}" }
# # => name - is too short (minimum is 5 characters)
# # name - can't be blank
# # address - can't be blank
def each
@errors.each_key { |attr| @errors[attr].each { |msg| yield attr, msg } }
end
# Yields each full error message added. So Person.errors.add("first_name", "can't be empty") will be returned
# Yields each full error message added. So <tt>Person.errors.add("first_name", "can't be empty")</tt> will be returned
# through iteration as "First name can't be empty".
#
# class Company < ActiveRecord::Base
@ -148,10 +175,10 @@ module ActiveRecord
# end
#
# company = Company.create(:address => '123 First St.')
# company.errors.each_full{|msg| puts msg } # =>
# Name is too short (minimum is 5 characters)
# Name can't be blank
# Address can't be blank
# company.errors.each_full{|msg| puts msg }
# # => Name is too short (minimum is 5 characters)
# # Name can't be blank
# # Address can't be blank
def each_full
full_messages.each { |msg| yield msg }
end
@ -166,22 +193,24 @@ module ActiveRecord
# company = Company.create(:address => '123 First St.')
# company.errors.full_messages # =>
# ["Name is too short (minimum is 5 characters)", "Name can't be blank", "Address can't be blank"]
def full_messages
def full_messages(options = {})
full_messages = []
@errors.each_key do |attr|
@errors[attr].each do |msg|
next if msg.nil?
@errors[attr].each do |message|
next unless message
if attr == "base"
full_messages << msg
full_messages << message
else
full_messages << @base.class.human_attribute_name(attr) + " " + msg
#key = :"activerecord.att.#{@base.class.name.underscore.to_sym}.#{attr}"
attr_name = @base.class.human_attribute_name(attr)
full_messages << attr_name + ' ' + message
end
end
end
full_messages
end
end
# Returns true if no errors have been added.
def empty?
@ -209,13 +238,13 @@ module ActiveRecord
# end
#
# company = Company.create(:address => '123 First St.')
# company.errors.to_xml # =>
# <?xml version="1.0" encoding="UTF-8"?>
# <errors>
# <error>Name is too short (minimum is 5 characters)</error>
# <error>Name can't be blank</error>
# <error>Address can't be blank</error>
# </errors>
# company.errors.to_xml
# # => <?xml version="1.0" encoding="UTF-8"?>
# # <errors>
# # <error>Name is too short (minimum is 5 characters)</error>
# # <error>Name can't be blank</error>
# # <error>Address can't be blank</error>
# # </errors>
def to_xml(options={})
options[:root] ||= "errors"
options[:indent] ||= 2
@ -226,9 +255,12 @@ module ActiveRecord
full_messages.each { |msg| e.error(msg) }
end
end
end
# Please do have a look at ActiveRecord::Validations::ClassMethods for a higher level of validations.
#
# Active Records implement validation by overwriting Base#validate (or the variations, +validate_on_create+ and
# +validate_on_update+). Each of these methods can inspect the state of the object, which usually means ensuring
# that a number of attributes have a certain value (such as not empty, within a given range, matching a certain regular expression).
@ -261,14 +293,12 @@ module ActiveRecord
# person.errors.on "phone_number" # => "has invalid format"
# person.errors.each_full { |msg| puts msg }
# # => "Last name can't be empty\n" +
# "Phone number has invalid format"
# # "Phone number has invalid format"
#
# person.attributes = { "last_name" => "Heinemeier", "phone_number" => "555-555" }
# person.save # => true (and person is now saved in the database)
#
# An Errors object is automatically created for every Active Record.
#
# Please do have a look at ActiveRecord::Validations::ClassMethods for a higher level of validations.
module Validations
VALIDATIONS = %w( validate validate_on_create validate_on_update )
@ -277,16 +307,56 @@ module ActiveRecord
base.class_eval do
alias_method_chain :save, :validation
alias_method_chain :save!, :validation
alias_method_chain :update_attribute, :validation_skipping
end
base.send :include, ActiveSupport::Callbacks
base.define_callbacks *VALIDATIONS
end
# All of the following validations are defined in the class scope of the model that you're interested in validating.
# They offer a more declarative way of specifying when the model is valid and when it is not. It is recommended to use
# these over the low-level calls to +validate+ and +validate_on_create+ when possible.
# Active Record classes can implement validations in several ways. The highest level, easiest to read,
# and recommended approach is to use the declarative <tt>validates_..._of</tt> class methods (and
# +validates_associated+) documented below. These are sufficient for most model validations.
#
# Slightly lower level is +validates_each+. It provides some of the same options as the purely declarative
# validation methods, but like all the lower-level approaches it requires manually adding to the errors collection
# when the record is invalid.
#
# At a yet lower level, a model can use the class methods +validate+, +validate_on_create+ and +validate_on_update+
# to add validation methods or blocks. These are ActiveSupport::Callbacks and follow the same rules of inheritance
# and chaining.
#
# The lowest level style is to define the instance methods +validate+, +validate_on_create+ and +validate_on_update+
# as documented in ActiveRecord::Validations.
#
# == +validate+, +validate_on_create+ and +validate_on_update+ Class Methods
#
# Calls to these methods add a validation method or block to the class. Again, this approach is recommended
# only when the higher-level methods documented below (<tt>validates_..._of</tt> and +validates_associated+) are
# insufficient to handle the required validation.
#
# This can be done with a symbol pointing to a method:
#
# class Comment < ActiveRecord::Base
# validate :must_be_friends
#
# def must_be_friends
# errors.add_to_base("Must be friends to leave a comment") unless commenter.friend_of?(commentee)
# end
# end
#
# Or with a block which is passed the current record to be validated:
#
# class Comment < ActiveRecord::Base
# validate do |comment|
# comment.must_be_friends
# end
#
# def must_be_friends
# errors.add_to_base("Must be friends to leave a comment") unless commenter.friend_of?(commentee)
# end
# end
#
# This usage applies to +validate_on_create+ and +validate_on_update+ as well.
module ClassMethods
DEFAULT_VALIDATION_OPTIONS = {
:on => :save,
@ -300,34 +370,6 @@ module ActiveRecord
:equal_to => '==', :less_than => '<', :less_than_or_equal_to => '<=',
:odd => 'odd?', :even => 'even?' }.freeze
# Adds a validation method or block to the class. This is useful when
# overriding the +validate+ instance method becomes too unwieldly and
# you're looking for more descriptive declaration of your validations.
#
# This can be done with a symbol pointing to a method:
#
# class Comment < ActiveRecord::Base
# validate :must_be_friends
#
# def must_be_friends
# errors.add_to_base("Must be friends to leave a comment") unless commenter.friend_of?(commentee)
# end
# end
#
# Or with a block which is passed the current record to be validated:
#
# class Comment < ActiveRecord::Base
# validate do |comment|
# comment.must_be_friends
# end
#
# def must_be_friends
# errors.add_to_base("Must be friends to leave a comment") unless commenter.friend_of?(commentee)
# end
# end
#
# This usage applies to +validate_on_create+ and +validate_on_update+ as well.
# Validates each attribute against a block.
#
# class Person < ActiveRecord::Base
@ -389,13 +431,15 @@ module ActiveRecord
# not occur (e.g. <tt>:unless => :skip_validation</tt>, or <tt>:unless => Proc.new { |user| user.signup_step <= 2 }</tt>). The
# method, proc or string should return or evaluate to a true or false value.
def validates_confirmation_of(*attr_names)
configuration = { :message => ActiveRecord::Errors.default_error_messages[:confirmation], :on => :save }
configuration = { :on => :save }
configuration.update(attr_names.extract_options!)
attr_accessor(*(attr_names.map { |n| "#{n}_confirmation" }))
validates_each(attr_names, configuration) do |record, attr_name, value|
record.errors.add(attr_name, configuration[:message]) unless record.send("#{attr_name}_confirmation").nil? or value == record.send("#{attr_name}_confirmation")
unless record.send("#{attr_name}_confirmation").nil? or value == record.send("#{attr_name}_confirmation")
record.errors.add(attr_name, :confirmation, :default => configuration[:message])
end
end
end
@ -423,19 +467,21 @@ module ActiveRecord
# not occur (e.g. <tt>:unless => :skip_validation</tt>, or <tt>:unless => Proc.new { |user| user.signup_step <= 2 }</tt>). The
# method, proc or string should return or evaluate to a true or false value.
def validates_acceptance_of(*attr_names)
configuration = { :message => ActiveRecord::Errors.default_error_messages[:accepted], :on => :save, :allow_nil => true, :accept => "1" }
configuration = { :on => :save, :allow_nil => true, :accept => "1" }
configuration.update(attr_names.extract_options!)
db_cols = begin
column_names
rescue ActiveRecord::StatementInvalid
rescue Exception # To ignore both statement and connection errors
[]
end
names = attr_names.reject { |name| db_cols.include?(name.to_s) }
attr_accessor(*names)
validates_each(attr_names,configuration) do |record, attr_name, value|
record.errors.add(attr_name, configuration[:message]) unless value == configuration[:accept]
unless value == configuration[:accept]
record.errors.add(attr_name, :accepted, :default => configuration[:message])
end
end
end
@ -462,7 +508,7 @@ module ActiveRecord
# method, proc or string should return or evaluate to a true or false value.
#
def validates_presence_of(*attr_names)
configuration = { :message => ActiveRecord::Errors.default_error_messages[:blank], :on => :save }
configuration = { :on => :save }
configuration.update(attr_names.extract_options!)
# can't use validates_each here, because it cannot cope with nonexistent attributes,
@ -476,13 +522,13 @@ module ActiveRecord
#
# class Person < ActiveRecord::Base
# validates_length_of :first_name, :maximum=>30
# validates_length_of :last_name, :maximum=>30, :message=>"less than %d if you don't mind"
# validates_length_of :last_name, :maximum=>30, :message=>"less than {{count}} if you don't mind"
# validates_length_of :fax, :in => 7..32, :allow_nil => true
# validates_length_of :phone, :in => 7..32, :allow_blank => true
# validates_length_of :user_name, :within => 6..20, :too_long => "pick a shorter name", :too_short => "pick a longer name"
# validates_length_of :fav_bra_size, :minimum => 1, :too_short => "please enter at least %d character"
# validates_length_of :smurf_leader, :is => 4, :message => "papa is spelled with %d characters... don't play me."
# validates_length_of :essay, :minimum => 100, :too_short => "Your essay must be at least %d words."), :tokenizer => lambda {|str| str.scan(/\w+/) }
# validates_length_of :fav_bra_size, :minimum => 1, :too_short => "please enter at least {{count}} character"
# validates_length_of :smurf_leader, :is => 4, :message => "papa is spelled with {{count}} characters... don't play me."
# validates_length_of :essay, :minimum => 100, :too_short => "Your essay must be at least {{count}} words."), :tokenizer => lambda {|str| str.scan(/\w+/) }
# end
#
# Configuration options:
@ -493,9 +539,9 @@ module ActiveRecord
# * <tt>:in</tt> - A synonym(or alias) for <tt>:within</tt>.
# * <tt>:allow_nil</tt> - Attribute may be +nil+; skip validation.
# * <tt>:allow_blank</tt> - Attribute may be blank; skip validation.
# * <tt>:too_long</tt> - The error message if the attribute goes over the maximum (default is: "is too long (maximum is %d characters)").
# * <tt>:too_short</tt> - The error message if the attribute goes under the minimum (default is: "is too short (min is %d characters)").
# * <tt>:wrong_length</tt> - The error message if using the <tt>:is</tt> method and the attribute is the wrong size (default is: "is the wrong length (should be %d characters)").
# * <tt>:too_long</tt> - The error message if the attribute goes over the maximum (default is: "is too long (maximum is {{count}} characters)").
# * <tt>:too_short</tt> - The error message if the attribute goes under the minimum (default is: "is too short (min is {{count}} characters)").
# * <tt>:wrong_length</tt> - The error message if using the <tt>:is</tt> method and the attribute is the wrong size (default is: "is the wrong length (should be {{count}} characters)").
# * <tt>:message</tt> - The error message to use for a <tt>:minimum</tt>, <tt>:maximum</tt>, or <tt>:is</tt> violation. An alias of the appropriate <tt>too_long</tt>/<tt>too_short</tt>/<tt>wrong_length</tt> message.
# * <tt>:on</tt> - Specifies when this validation is active (default is <tt>:save</tt>, other options <tt>:create</tt>, <tt>:update</tt>).
# * <tt>:if</tt> - Specifies a method, proc or string to call to determine if the validation should
@ -510,10 +556,7 @@ module ActiveRecord
def validates_length_of(*attrs)
# Merge given options with defaults.
options = {
:too_long => ActiveRecord::Errors.default_error_messages[:too_long],
:too_short => ActiveRecord::Errors.default_error_messages[:too_short],
:wrong_length => ActiveRecord::Errors.default_error_messages[:wrong_length],
:tokenizer => lambda {|value| value.split(//)}
:tokenizer => lambda {|value| value.split(//)}
}.merge(DEFAULT_VALIDATION_OPTIONS)
options.update(attrs.extract_options!.symbolize_keys)
@ -536,15 +579,12 @@ module ActiveRecord
when :within, :in
raise ArgumentError, ":#{option} must be a Range" unless option_value.is_a?(Range)
too_short = options[:too_short] % option_value.begin
too_long = options[:too_long] % option_value.end
validates_each(attrs, options) do |record, attr, value|
value = options[:tokenizer].call(value) if value.kind_of?(String)
if value.nil? or value.size < option_value.begin
record.errors.add(attr, too_short)
record.errors.add(attr, :too_short, :default => options[:too_short], :count => option_value.begin)
elsif value.size > option_value.end
record.errors.add(attr, too_long)
record.errors.add(attr, :too_long, :default => options[:too_long], :count => option_value.end)
end
end
when :is, :minimum, :maximum
@ -554,11 +594,13 @@ module ActiveRecord
validity_checks = { :is => "==", :minimum => ">=", :maximum => "<=" }
message_options = { :is => :wrong_length, :minimum => :too_short, :maximum => :too_long }
message = (options[:message] || options[message_options[option]]) % option_value
validates_each(attrs, options) do |record, attr, value|
value = options[:tokenizer].call(value) if value.kind_of?(String)
record.errors.add(attr, message) unless !value.nil? and value.size.method(validity_checks[option])[option_value]
unless !value.nil? and value.size.method(validity_checks[option])[option_value]
key = message_options[option]
custom_message = options[:message] || options[key]
record.errors.add(attr, key, :default => custom_message, :count => option_value)
end
end
end
end
@ -583,14 +625,10 @@ module ActiveRecord
# When the record is created, a check is performed to make sure that no record exists in the database with the given value for the specified
# attribute (that maps to a column). When the record is updated, the same check is made but disregarding the record itself.
#
# Because this check is performed outside the database there is still a chance that duplicate values
# will be inserted in two parallel transactions. To guarantee against this you should create a
# unique index on the field. See +add_index+ for more information.
#
# Configuration options:
# * <tt>:message</tt> - Specifies a custom error message (default is: "has already been taken").
# * <tt>:scope</tt> - One or more columns by which to limit the scope of the uniqueness constraint.
# * <tt>:case_sensitive</tt> - Looks for an exact match. Ignored by non-text columns (+false+ by default).
# * <tt>:case_sensitive</tt> - Looks for an exact match. Ignored by non-text columns (+true+ by default).
# * <tt>:allow_nil</tt> - If set to true, skips this validation if the attribute is +nil+ (default is +false+).
# * <tt>:allow_blank</tt> - If set to true, skips this validation if the attribute is blank (default is +false+).
# * <tt>:if</tt> - Specifies a method, proc or string to call to determine if the validation should
@ -599,8 +637,72 @@ module ActiveRecord
# * <tt>:unless</tt> - Specifies a method, proc or string to call to determine if the validation should
# not occur (e.g. <tt>:unless => :skip_validation</tt>, or <tt>:unless => Proc.new { |user| user.signup_step <= 2 }</tt>). The
# method, proc or string should return or evaluate to a true or false value.
#
# === Concurrency and integrity
#
# Using this validation method in conjunction with ActiveRecord::Base#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
# the same time, and a Comment's title must be unique. At the database-level,
# the actions performed by these users could be interleaved in the following manner:
#
# User 1 | User 2
# ------------------------------------+--------------------------------------
# # User 1 checks whether there's |
# # already a comment with the title |
# # 'My Post'. This is not the case. |
# SELECT * FROM comments |
# WHERE title = 'My Post' |
# |
# | # User 2 does the same thing and also
# | # infers that his title is unique.
# | SELECT * FROM comments
# | WHERE title = 'My Post'
# |
# # User 1 inserts his comment. |
# INSERT INTO comments |
# (title, content) VALUES |
# ('My Post', 'hi!') |
# |
# | # User 2 does the same thing.
# | INSERT INTO comments
# | (title, content) VALUES
# | ('My Post', 'hello!')
# |
# | # ^^^^^^
# | # Boom! We now have a duplicate
# | # title!
#
# This could even happen if you use transactions with the 'serializable'
# isolation level. There are several ways to get around this problem:
# - By locking the database table before validating, and unlocking it after
# saving. However, table locking is very expensive, and thus not
# recommended.
# - By locking a lock file before validating, and unlocking it after saving.
# This does not work if you've scaled your Rails application across
# multiple web servers (because they cannot share lock files, or cannot
# do that efficiently), and thus not recommended.
# - Creating a unique index on the field, by using
# ActiveRecord::ConnectionAdapters::SchemaStatements#add_index. In the
# rare case that a race condition occurs, the database will guarantee
# the field's uniqueness.
#
# When the database catches such a duplicate insertion,
# ActiveRecord::Base#save will raise an ActiveRecord::StatementInvalid
# exception. You can either choose to let this error propagate (which
# will result in the default Rails exception page being shown), or you
# can catch it and restart the transaction (e.g. by telling the user
# that the title already exists, and asking him to re-enter the title).
# This technique is also known as optimistic concurrency control:
# http://en.wikipedia.org/wiki/Optimistic_concurrency_control
#
# Active Record currently provides no way to distinguish unique
# index constraint errors from other types of database errors, so you
# will have to parse the (database-specific) exception message to detect
# such a case.
def validates_uniqueness_of(*attr_names)
configuration = { :message => ActiveRecord::Errors.default_error_messages[:taken], :case_sensitive => true }
configuration = { :case_sensitive => true }
configuration.update(attr_names.extract_options!)
validates_each(attr_names,configuration) do |record, attr_name, value|
@ -620,18 +722,23 @@ module ActiveRecord
is_text_column = finder_class.columns_hash[attr_name.to_s].text?
if !value.nil? && is_text_column
if value.nil?
comparison_operator = "IS ?"
elsif is_text_column
comparison_operator = "#{connection.case_sensitive_equality_operator} ?"
value = value.to_s
else
comparison_operator = "= ?"
end
sql_attribute = "#{record.class.quoted_table_name}.#{connection.quote_column_name(attr_name)}"
if value.nil? || (configuration[:case_sensitive] || !is_text_column)
condition_sql = "#{record.class.quoted_table_name}.#{attr_name} #{attribute_condition(value)}"
condition_sql = "#{sql_attribute} #{comparison_operator}"
condition_params = [value]
else
# sqlite has case sensitive SELECT query, while MySQL/Postgresql don't.
# Hence, this is needed only for sqlite.
condition_sql = "LOWER(#{record.class.quoted_table_name}.#{attr_name}) #{attribute_condition(value)}"
condition_params = [value.chars.downcase]
condition_sql = "LOWER(#{sql_attribute}) #{comparison_operator}"
condition_params = [value.mb_chars.downcase]
end
if scope = configuration[:scope]
@ -647,26 +754,10 @@ module ActiveRecord
condition_params << record.send(:id)
end
results = finder_class.with_exclusive_scope do
connection.select_all(
construct_finder_sql(
:select => "#{connection.quote_column_name(attr_name)}",
:from => "#{finder_class.quoted_table_name}",
:conditions => [condition_sql, *condition_params]
)
)
end
unless results.length.zero?
found = true
# As MySQL/Postgres don't have case sensitive SELECT queries, we try to find duplicate
# column in ruby when case sensitive option
if configuration[:case_sensitive] && finder_class.columns_hash[attr_name.to_s].text?
found = results.any? { |a| a[attr_name.to_s] == value }
finder_class.with_exclusive_scope do
if finder_class.exists?([condition_sql, *condition_params])
record.errors.add(attr_name, :taken, :default => configuration[:message], :value => value)
end
record.errors.add(attr_name, configuration[:message]) if found
end
end
end
@ -696,13 +787,15 @@ module ActiveRecord
# not occur (e.g. <tt>:unless => :skip_validation</tt>, or <tt>:unless => Proc.new { |user| user.signup_step <= 2 }</tt>). The
# method, proc or string should return or evaluate to a true or false value.
def validates_format_of(*attr_names)
configuration = { :message => ActiveRecord::Errors.default_error_messages[:invalid], :on => :save, :with => nil }
configuration = { :on => :save, :with => nil }
configuration.update(attr_names.extract_options!)
raise(ArgumentError, "A regular expression must be supplied as the :with option of the configuration hash") unless configuration[:with].is_a?(Regexp)
validates_each(attr_names, configuration) do |record, attr_name, value|
record.errors.add(attr_name, configuration[:message] % value) unless value.to_s =~ configuration[:with]
unless value.to_s =~ configuration[:with]
record.errors.add(attr_name, :invalid, :default => configuration[:message], :value => value)
end
end
end
@ -711,7 +804,7 @@ module ActiveRecord
# class Person < ActiveRecord::Base
# validates_inclusion_of :gender, :in => %w( m f ), :message => "woah! what are you then!??!!"
# validates_inclusion_of :age, :in => 0..99
# validates_inclusion_of :format, :in => %w( jpg gif png ), :message => "extension %s is not included in the list"
# validates_inclusion_of :format, :in => %w( jpg gif png ), :message => "extension {{value}} is not included in the list"
# end
#
# Configuration options:
@ -726,15 +819,17 @@ module ActiveRecord
# not occur (e.g. <tt>:unless => :skip_validation</tt>, or <tt>:unless => Proc.new { |user| user.signup_step <= 2 }</tt>). The
# method, proc or string should return or evaluate to a true or false value.
def validates_inclusion_of(*attr_names)
configuration = { :message => ActiveRecord::Errors.default_error_messages[:inclusion], :on => :save }
configuration = { :on => :save }
configuration.update(attr_names.extract_options!)
enum = configuration[:in] || configuration[:within]
raise(ArgumentError, "An object with the method include? is required must be supplied as the :in option of the configuration hash") unless enum.respond_to?("include?")
raise(ArgumentError, "An object with the method include? is required must be supplied as the :in option of the configuration hash") unless enum.respond_to?(:include?)
validates_each(attr_names, configuration) do |record, attr_name, value|
record.errors.add(attr_name, configuration[:message] % value) unless enum.include?(value)
unless enum.include?(value)
record.errors.add(attr_name, :inclusion, :default => configuration[:message], :value => value)
end
end
end
@ -743,7 +838,7 @@ module ActiveRecord
# class Person < ActiveRecord::Base
# validates_exclusion_of :username, :in => %w( admin superuser ), :message => "You don't belong here"
# validates_exclusion_of :age, :in => 30..60, :message => "This site is only for under 30 and over 60"
# validates_exclusion_of :format, :in => %w( mov avi ), :message => "extension %s is not allowed"
# validates_exclusion_of :format, :in => %w( mov avi ), :message => "extension {{value}} is not allowed"
# end
#
# Configuration options:
@ -758,15 +853,17 @@ module ActiveRecord
# not occur (e.g. <tt>:unless => :skip_validation</tt>, or <tt>:unless => Proc.new { |user| user.signup_step <= 2 }</tt>). The
# method, proc or string should return or evaluate to a true or false value.
def validates_exclusion_of(*attr_names)
configuration = { :message => ActiveRecord::Errors.default_error_messages[:exclusion], :on => :save }
configuration = { :on => :save }
configuration.update(attr_names.extract_options!)
enum = configuration[:in] || configuration[:within]
raise(ArgumentError, "An object with the method include? is required must be supplied as the :in option of the configuration hash") unless enum.respond_to?("include?")
raise(ArgumentError, "An object with the method include? is required must be supplied as the :in option of the configuration hash") unless enum.respond_to?(:include?)
validates_each(attr_names, configuration) do |record, attr_name, value|
record.errors.add(attr_name, configuration[:message] % value) if enum.include?(value)
if enum.include?(value)
record.errors.add(attr_name, :exclusion, :default => configuration[:message], :value => value)
end
end
end
@ -802,12 +899,13 @@ module ActiveRecord
# not occur (e.g. <tt>:unless => :skip_validation</tt>, or <tt>:unless => Proc.new { |user| user.signup_step <= 2 }</tt>). The
# method, proc or string should return or evaluate to a true or false value.
def validates_associated(*attr_names)
configuration = { :message => ActiveRecord::Errors.default_error_messages[:invalid], :on => :save }
configuration = { :on => :save }
configuration.update(attr_names.extract_options!)
validates_each(attr_names, configuration) do |record, attr_name, value|
record.errors.add(attr_name, configuration[:message]) unless
(value.is_a?(Array) ? value : [value]).inject(true) { |v, r| (r.nil? || r.valid?) && v }
unless (value.is_a?(Array) ? value : [value]).inject(true) { |v, r| (r.nil? || r.valid?) && v }
record.errors.add(attr_name, :invalid, :default => configuration[:message], :value => value)
end
end
end
@ -855,15 +953,15 @@ module ActiveRecord
if configuration[:only_integer]
unless raw_value.to_s =~ /\A[+-]?\d+\Z/
record.errors.add(attr_name, configuration[:message] || ActiveRecord::Errors.default_error_messages[:not_a_number])
record.errors.add(attr_name, :not_a_number, :value => raw_value, :default => configuration[:message])
next
end
raw_value = raw_value.to_i
else
begin
begin
raw_value = Kernel.Float(raw_value)
rescue ArgumentError, TypeError
record.errors.add(attr_name, configuration[:message] || ActiveRecord::Errors.default_error_messages[:not_a_number])
record.errors.add(attr_name, :not_a_number, :value => raw_value, :default => configuration[:message])
next
end
end
@ -871,11 +969,11 @@ module ActiveRecord
numericality_options.each do |option|
case option
when :odd, :even
record.errors.add(attr_name, configuration[:message] || ActiveRecord::Errors.default_error_messages[option]) unless raw_value.to_i.method(ALL_NUMERICALITY_CHECKS[option])[]
unless raw_value.to_i.method(ALL_NUMERICALITY_CHECKS[option])[]
record.errors.add(attr_name, option, :value => raw_value, :default => configuration[:message])
end
else
message = configuration[:message] || ActiveRecord::Errors.default_error_messages[option]
message = message % configuration[option] if configuration[option]
record.errors.add(attr_name, message) unless raw_value.method(ALL_NUMERICALITY_CHECKS[option])[configuration[option]]
record.errors.add(attr_name, option, :default => configuration[:message], :value => raw_value, :count => configuration[option]) unless raw_value.method(ALL_NUMERICALITY_CHECKS[option])[configuration[option]]
end
end
end
@ -924,14 +1022,6 @@ module ActiveRecord
end
end
# Updates a single attribute and saves the record without going through the normal validation procedure.
# This is especially useful for boolean flags on existing records. The regular +update_attribute+ method
# in Base is replaced with this when the validations module is mixed in, which it is by default.
def update_attribute_with_validation_skipping(name, value)
send(name.to_s + '=', value)
save(false)
end
# Runs +validate+ and +validate_on_create+ or +validate_on_update+ and returns true if no errors were added otherwise false.
def valid?
errors.clear

View file

@ -1,8 +1,8 @@
module ActiveRecord
module VERSION #:nodoc:
MAJOR = 2
MINOR = 1
TINY = 1
MINOR = 2
TINY = 0
STRING = [MAJOR, MINOR, TINY].join('.')
end