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:
parent
39348c65c2
commit
7600aef48b
827 changed files with 123652 additions and 11027 deletions
35
vendor/rails/activerecord/CHANGELOG
vendored
35
vendor/rails/activerecord/CHANGELOG
vendored
|
@ -1,16 +1,45 @@
|
|||
*2.1.1 (September 4th, 2008)*
|
||||
*2.2.0 [RC1] (October 24th, 2008)*
|
||||
|
||||
* Skip collection ids reader optimization if using :finder_sql [Jeremy Kemper]
|
||||
|
||||
* Add Model#delete instance method, similar to Model.delete class method. #1086 [Hongli Lai]
|
||||
|
||||
* MySQL: cope with quirky default values for not-null text columns. #1043 [Frederick Cheung]
|
||||
|
||||
* Multiparameter attributes skip time zone conversion for time-only columns [#1030 state:resolved] [Geoff Buesing]
|
||||
|
||||
* Base.skip_time_zone_conversion_for_attributes uses class_inheritable_accessor, so that subclasses don't overwrite Base [#346 state:resolved] [miloops]
|
||||
|
||||
* Added find_last_by dynamic finder #762 [miloops]
|
||||
|
||||
* Internal API: configurable association options and build_association method for reflections so plugins may extend and override. #985 [Hongli Lai]
|
||||
|
||||
* Changed benchmarks to be reported in milliseconds [DHH]
|
||||
|
||||
* Connection pooling. #936 [Nick Sieger]
|
||||
|
||||
* Merge scoped :joins together instead of overwriting them. May expose scoping bugs in your code! #501 [Andrew White]
|
||||
|
||||
* before_save, before_validation and before_destroy callbacks that return false will now ROLLBACK the transaction. Previously this would have been committed before the processing was aborted. #891 [Xavier Noria]
|
||||
|
||||
* Transactional migrations for databases which support them. #834 [divoxx, Adam Wiggins, Tarmo Tänav]
|
||||
|
||||
* Set config.active_record.timestamped_migrations = false to have migrations with numeric prefix instead of UTC timestamp. #446. [Andrew Stone, Nik Wakelin]
|
||||
|
||||
* Fixed that create database statements would always include "DEFAULT NULL" (Nick Sieger) [#334]
|
||||
|
||||
* change_column_default preserves the not-null constraint. #617 [Tarmo Tänav]
|
||||
|
||||
* Fixed that create database statements would always include "DEFAULT NULL" (Nick Sieger) [#334]
|
||||
|
||||
* Add :tokenizer option to validates_length_of to specify how to split up the attribute string. #507. [David Lowenfels] Example :
|
||||
|
||||
# Ensure essay contains at least 100 words.
|
||||
validates_length_of :essay, :minimum => 100, :too_short => "Your essay must be at least %d words."), :tokenizer => lambda {|str| str.scan(/\w+/) }
|
||||
|
||||
* Allow conditions on multiple tables to be specified using hash. [Pratik Naik]. Example:
|
||||
|
||||
User.all :joins => :items, :conditions => { :age => 10, :items => { :color => 'black' } }
|
||||
Item.first :conditions => { :items => { :color => 'red' } }
|
||||
|
||||
* Always treat integer :limit as byte length. #420 [Tarmo Tänav]
|
||||
|
||||
* Partial updates don't update lock_version if nothing changed. #426 [Daniel Morrison]
|
||||
|
|
9
vendor/rails/activerecord/Rakefile
vendored
9
vendor/rails/activerecord/Rakefile
vendored
|
@ -5,7 +5,6 @@ require 'rake/rdoctask'
|
|||
require 'rake/packagetask'
|
||||
require 'rake/gempackagetask'
|
||||
require 'rake/contrib/sshpublisher'
|
||||
require 'rake/contrib/rubyforgepublisher'
|
||||
|
||||
require File.join(File.dirname(__FILE__), 'lib', 'active_record', 'version')
|
||||
require File.expand_path(File.dirname(__FILE__)) + "/test/config"
|
||||
|
@ -31,7 +30,7 @@ desc 'Run mysql, sqlite, and postgresql tests by default'
|
|||
task :default => :test
|
||||
|
||||
desc 'Run mysql, sqlite, and postgresql tests'
|
||||
task :test => %w(test_mysql test_sqlite test_sqlite3 test_postgresql)
|
||||
task :test => %w(test_mysql test_sqlite3 test_postgresql)
|
||||
|
||||
for adapter in %w( mysql postgresql sqlite sqlite3 firebird db2 oracle sybase openbase frontbase )
|
||||
Rake::TestTask.new("test_#{adapter}") { |t|
|
||||
|
@ -172,7 +171,7 @@ spec = Gem::Specification.new do |s|
|
|||
s.files = s.files + Dir.glob( "#{dir}/**/*" ).delete_if { |item| item.include?( "\.svn" ) }
|
||||
end
|
||||
|
||||
s.add_dependency('activesupport', '= 2.1.1' + PKG_BUILD)
|
||||
s.add_dependency('activesupport', '= 2.2.0' + PKG_BUILD)
|
||||
|
||||
s.files.delete FIXTURES_ROOT + "/fixture_database.sqlite"
|
||||
s.files.delete FIXTURES_ROOT + "/fixture_database_2.sqlite"
|
||||
|
@ -226,8 +225,8 @@ end
|
|||
|
||||
desc "Publish the beta gem"
|
||||
task :pgem => [:package] do
|
||||
Rake::SshFilePublisher.new("david@greed.loudthinking.com", "/u/sites/gems/gems", "pkg", "#{PKG_FILE_NAME}.gem").upload
|
||||
`ssh david@greed.loudthinking.com '/u/sites/gems/gemupdate.sh'`
|
||||
Rake::SshFilePublisher.new("gems.rubyonrails.org", "/u/sites/gems/gems", "pkg", "#{PKG_FILE_NAME}.gem").upload
|
||||
`ssh gems.rubyonrails.org '/u/sites/gems/gemupdate.sh'`
|
||||
end
|
||||
|
||||
desc "Publish the API documentation"
|
||||
|
|
21
vendor/rails/activerecord/lib/active_record.rb
vendored
21
vendor/rails/activerecord/lib/active_record.rb
vendored
|
@ -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'
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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
|
@ -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
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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
|
||||
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -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)
|
||||
|
|
623
vendor/rails/activerecord/lib/active_record/base.rb
vendored
623
vendor/rails/activerecord/lib/active_record/base.rb
vendored
File diff suppressed because it is too large
Load diff
|
@ -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
|
||||
|
|
|
@ -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
|
||||
|
||||
|
|
349
vendor/rails/activerecord/lib/active_record/connection_adapters/abstract/connection_pool.rb
vendored
Normal file
349
vendor/rails/activerecord/lib/active_record/connection_adapters/abstract/connection_pool.rb
vendored
Normal 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
|
|
@ -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
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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.
|
||||
|
|
|
@ -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
|
||||
|
||||
|
|
|
@ -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
|
||||
|
||||
|
|
|
@ -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.
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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
|
||||
|
|
41
vendor/rails/activerecord/lib/active_record/dynamic_finder_match.rb
vendored
Normal file
41
vendor/rails/activerecord/lib/active_record/dynamic_finder_match.rb
vendored
Normal 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
|
|
@ -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
|
||||
|
|
26
vendor/rails/activerecord/lib/active_record/i18n_interpolation_deprecation.rb
vendored
Normal file
26
vendor/rails/activerecord/lib/active_record/i18n_interpolation_deprecation.rb
vendored
Normal 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
|
54
vendor/rails/activerecord/lib/active_record/locale/en-US.yml
vendored
Normal file
54
vendor/rails/activerecord/lib/active_record/locale/en-US.yml
vendored
Normal 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"
|
||||
|
|
@ -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
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -25,6 +25,11 @@ class ActiveSchemaTest < ActiveRecord::TestCase
|
|||
assert_equal "CREATE DATABASE `aimonetti` DEFAULT CHARACTER SET `latin1`", create_database(:aimonetti, {:charset => 'latin1'})
|
||||
assert_equal "CREATE DATABASE `matt_aimonetti` DEFAULT CHARACTER SET `big5` COLLATE `big5_chinese_ci`", create_database(:matt_aimonetti, {:charset => :big5, :collation => :big5_chinese_ci})
|
||||
end
|
||||
|
||||
def test_recreate_mysql_database_with_encoding
|
||||
create_database(:luca, {:charset => 'latin1'})
|
||||
assert_equal "CREATE DATABASE `luca` DEFAULT CHARACTER SET `latin1`", recreate_database(:luca, {:charset => 'latin1'})
|
||||
end
|
||||
end
|
||||
|
||||
def test_add_column
|
||||
|
|
|
@ -65,6 +65,12 @@ class AdapterTest < ActiveRecord::TestCase
|
|||
end
|
||||
end
|
||||
|
||||
if current_adapter?(:PostgreSQLAdapter)
|
||||
def test_encoding
|
||||
assert_not_nil @connection.encoding
|
||||
end
|
||||
end
|
||||
|
||||
def test_table_alias
|
||||
def @connection.test_table_alias_length() 10; end
|
||||
class << @connection
|
||||
|
|
|
@ -107,6 +107,45 @@ class AggregationsTest < ActiveRecord::TestCase
|
|||
customers(:david).gps_location = nil
|
||||
assert_equal nil, customers(:david).gps_location
|
||||
end
|
||||
|
||||
def test_custom_constructor
|
||||
assert_equal 'Barney GUMBLE', customers(:barney).fullname.to_s
|
||||
assert_kind_of Fullname, customers(:barney).fullname
|
||||
end
|
||||
|
||||
def test_custom_converter
|
||||
customers(:barney).fullname = 'Barnoit Gumbleau'
|
||||
assert_equal 'Barnoit GUMBLEAU', customers(:barney).fullname.to_s
|
||||
assert_kind_of Fullname, customers(:barney).fullname
|
||||
end
|
||||
end
|
||||
|
||||
class DeprecatedAggregationsTest < ActiveRecord::TestCase
|
||||
class Person < ActiveRecord::Base; end
|
||||
|
||||
def test_conversion_block_is_deprecated
|
||||
assert_deprecated 'conversion block has been deprecated' do
|
||||
Person.composed_of(:balance, :class_name => "Money", :mapping => %w(balance amount)) { |balance| balance.to_money }
|
||||
end
|
||||
end
|
||||
|
||||
def test_conversion_block_used_when_converter_option_is_nil
|
||||
assert_deprecated 'conversion block has been deprecated' do
|
||||
Person.composed_of(:balance, :class_name => "Money", :mapping => %w(balance amount)) { |balance| balance.to_money }
|
||||
end
|
||||
assert_raise(NoMethodError) { Person.new.balance = 5 }
|
||||
end
|
||||
|
||||
def test_converter_option_overrides_conversion_block
|
||||
assert_deprecated 'conversion block has been deprecated' do
|
||||
Person.composed_of(:balance, :class_name => "Money", :mapping => %w(balance amount), :converter => Proc.new { |balance| Money.new(balance) }) { |balance| balance.to_money }
|
||||
end
|
||||
|
||||
person = Person.new
|
||||
assert_nothing_raised { person.balance = 5 }
|
||||
assert_equal 5, person.balance.amount
|
||||
assert_kind_of Money, person.balance
|
||||
end
|
||||
end
|
||||
|
||||
class OverridingAggregationsTest < ActiveRecord::TestCase
|
||||
|
|
|
@ -428,4 +428,14 @@ class BelongsToAssociationsTest < ActiveRecord::TestCase
|
|||
assert log.valid?
|
||||
assert log.save
|
||||
end
|
||||
|
||||
def test_belongs_to_proxy_should_not_respond_to_private_methods
|
||||
assert_raises(NoMethodError) { companies(:first_firm).private_method }
|
||||
assert_raises(NoMethodError) { companies(:second_client).firm.private_method }
|
||||
end
|
||||
|
||||
def test_belongs_to_proxy_should_respond_to_private_methods_via_send
|
||||
companies(:first_firm).send(:private_method)
|
||||
companies(:second_client).firm.send(:private_method)
|
||||
end
|
||||
end
|
||||
|
|
|
@ -1,5 +1,20 @@
|
|||
require 'cases/helper'
|
||||
|
||||
module Remembered
|
||||
def self.included(base)
|
||||
base.extend ClassMethods
|
||||
base.class_eval do
|
||||
after_create :remember
|
||||
protected
|
||||
def remember; self.class.remembered << self; end
|
||||
end
|
||||
end
|
||||
|
||||
module ClassMethods
|
||||
def remembered; @@remembered ||= []; end
|
||||
def rand; @@remembered.rand; end
|
||||
end
|
||||
end
|
||||
|
||||
class ShapeExpression < ActiveRecord::Base
|
||||
belongs_to :shape, :polymorphic => true
|
||||
|
@ -8,26 +23,33 @@ end
|
|||
|
||||
class Circle < ActiveRecord::Base
|
||||
has_many :shape_expressions, :as => :shape
|
||||
include Remembered
|
||||
end
|
||||
class Square < ActiveRecord::Base
|
||||
has_many :shape_expressions, :as => :shape
|
||||
include Remembered
|
||||
end
|
||||
class Triangle < ActiveRecord::Base
|
||||
has_many :shape_expressions, :as => :shape
|
||||
include Remembered
|
||||
end
|
||||
class PaintColor < ActiveRecord::Base
|
||||
has_many :shape_expressions, :as => :paint
|
||||
belongs_to :non_poly, :foreign_key => "non_poly_one_id", :class_name => "NonPolyOne"
|
||||
include Remembered
|
||||
end
|
||||
class PaintTexture < ActiveRecord::Base
|
||||
has_many :shape_expressions, :as => :paint
|
||||
belongs_to :non_poly, :foreign_key => "non_poly_two_id", :class_name => "NonPolyTwo"
|
||||
include Remembered
|
||||
end
|
||||
class NonPolyOne < ActiveRecord::Base
|
||||
has_many :paint_colors
|
||||
include Remembered
|
||||
end
|
||||
class NonPolyTwo < ActiveRecord::Base
|
||||
has_many :paint_textures
|
||||
include Remembered
|
||||
end
|
||||
|
||||
|
||||
|
@ -49,23 +71,19 @@ class EagerLoadPolyAssocsTest < ActiveRecord::TestCase
|
|||
end
|
||||
|
||||
|
||||
# meant to be supplied as an ID, never returns 0
|
||||
def rand_simple
|
||||
val = (NUM_SIMPLE_OBJS * rand).round
|
||||
val == 0 ? 1 : val
|
||||
end
|
||||
|
||||
def generate_test_object_graphs
|
||||
1.upto(NUM_SIMPLE_OBJS) do
|
||||
[Circle, Square, Triangle, NonPolyOne, NonPolyTwo].map(&:create!)
|
||||
end
|
||||
1.upto(NUM_SIMPLE_OBJS) do |i|
|
||||
PaintColor.create!(:non_poly_one_id => rand_simple)
|
||||
PaintTexture.create!(:non_poly_two_id => rand_simple)
|
||||
1.upto(NUM_SIMPLE_OBJS) do
|
||||
PaintColor.create!(:non_poly_one_id => NonPolyOne.rand.id)
|
||||
PaintTexture.create!(:non_poly_two_id => NonPolyTwo.rand.id)
|
||||
end
|
||||
1.upto(NUM_SHAPE_EXPRESSIONS) do |i|
|
||||
ShapeExpression.create!(:shape_type => [Circle, Square, Triangle].rand.to_s, :shape_id => rand_simple,
|
||||
:paint_type => [PaintColor, PaintTexture].rand.to_s, :paint_id => rand_simple)
|
||||
1.upto(NUM_SHAPE_EXPRESSIONS) do
|
||||
shape_type = [Circle, Square, Triangle].rand
|
||||
paint_type = [PaintColor, PaintTexture].rand
|
||||
ShapeExpression.create!(:shape_type => shape_type.to_s, :shape_id => shape_type.rand.id,
|
||||
:paint_type => paint_type.to_s, :paint_id => paint_type.rand.id)
|
||||
end
|
||||
end
|
||||
|
||||
|
|
|
@ -18,7 +18,7 @@ require 'models/developer'
|
|||
require 'models/project'
|
||||
|
||||
class EagerAssociationTest < ActiveRecord::TestCase
|
||||
fixtures :posts, :comments, :authors, :categories, :categories_posts,
|
||||
fixtures :posts, :comments, :authors, :author_addresses, :categories, :categories_posts,
|
||||
:companies, :accounts, :tags, :taggings, :people, :readers,
|
||||
:owners, :pets, :author_favorites, :jobs, :references, :subscribers, :subscriptions, :books,
|
||||
:developers, :projects, :developers_projects
|
||||
|
@ -111,11 +111,58 @@ class EagerAssociationTest < ActiveRecord::TestCase
|
|||
end
|
||||
end
|
||||
|
||||
def test_finding_with_includes_on_has_many_association_with_same_include_includes_only_once
|
||||
author_id = authors(:david).id
|
||||
author = assert_queries(3) { Author.find(author_id, :include => {:posts_with_comments => :comments}) } # find the author, then find the posts, then find the comments
|
||||
author.posts_with_comments.each do |post_with_comments|
|
||||
assert_equal post_with_comments.comments.length, post_with_comments.comments.count
|
||||
assert_equal nil, post_with_comments.comments.uniq!
|
||||
end
|
||||
end
|
||||
|
||||
def test_finding_with_includes_on_has_one_assocation_with_same_include_includes_only_once
|
||||
author = authors(:david)
|
||||
post = author.post_about_thinking_with_last_comment
|
||||
last_comment = post.last_comment
|
||||
author = assert_queries(3) { Author.find(author.id, :include => {:post_about_thinking_with_last_comment => :last_comment})} # find the author, then find the posts, then find the comments
|
||||
assert_no_queries do
|
||||
assert_equal post, author.post_about_thinking_with_last_comment
|
||||
assert_equal last_comment, author.post_about_thinking_with_last_comment.last_comment
|
||||
end
|
||||
end
|
||||
|
||||
def test_finding_with_includes_on_belongs_to_association_with_same_include_includes_only_once
|
||||
post = posts(:welcome)
|
||||
author = post.author
|
||||
author_address = author.author_address
|
||||
post = assert_queries(3) { Post.find(post.id, :include => {:author_with_address => :author_address}) } # find the post, then find the author, then find the address
|
||||
assert_no_queries do
|
||||
assert_equal author, post.author_with_address
|
||||
assert_equal author_address, post.author_with_address.author_address
|
||||
end
|
||||
end
|
||||
|
||||
def test_finding_with_includes_on_null_belongs_to_association_with_same_include_includes_only_once
|
||||
post = posts(:welcome)
|
||||
post.update_attributes!(:author => nil)
|
||||
post = assert_queries(2) { Post.find(post.id, :include => {:author_with_address => :author_address}) } # find the post, then find the author which is null so no query for the address
|
||||
assert_no_queries do
|
||||
assert_equal nil, post.author_with_address
|
||||
end
|
||||
end
|
||||
|
||||
def test_loading_from_an_association
|
||||
posts = authors(:david).posts.find(:all, :include => :comments, :order => "posts.id")
|
||||
assert_equal 2, posts.first.comments.size
|
||||
end
|
||||
|
||||
def test_loading_from_an_association_that_has_a_hash_of_conditions
|
||||
assert_nothing_raised do
|
||||
Author.find(:all, :include => :hello_posts_with_hash_conditions)
|
||||
end
|
||||
assert !Author.find(authors(:david).id, :include => :hello_posts_with_hash_conditions).hello_posts.empty?
|
||||
end
|
||||
|
||||
def test_loading_with_no_associations
|
||||
assert_nil Post.find(posts(:authorless).id, :include => :author).author
|
||||
end
|
||||
|
@ -260,14 +307,23 @@ class EagerAssociationTest < ActiveRecord::TestCase
|
|||
end
|
||||
|
||||
def test_eager_with_has_many_through
|
||||
posts_with_comments = people(:michael).posts.find(:all, :include => :comments)
|
||||
posts_with_author = people(:michael).posts.find(:all, :include => :author )
|
||||
posts_with_comments_and_author = people(:michael).posts.find(:all, :include => [ :comments, :author ])
|
||||
posts_with_comments = people(:michael).posts.find(:all, :include => :comments, :order => 'posts.id')
|
||||
posts_with_author = people(:michael).posts.find(:all, :include => :author, :order => 'posts.id')
|
||||
posts_with_comments_and_author = people(:michael).posts.find(:all, :include => [ :comments, :author ], :order => 'posts.id')
|
||||
assert_equal 2, posts_with_comments.inject(0) { |sum, post| sum += post.comments.size }
|
||||
assert_equal authors(:david), assert_no_queries { posts_with_author.first.author }
|
||||
assert_equal authors(:david), assert_no_queries { posts_with_comments_and_author.first.author }
|
||||
end
|
||||
|
||||
def test_eager_with_has_many_through_a_belongs_to_association
|
||||
author = authors(:mary)
|
||||
post = Post.create!(:author => author, :title => "TITLE", :body => "BODY")
|
||||
author.author_favorites.create(:favorite_author_id => 1)
|
||||
author.author_favorites.create(:favorite_author_id => 2)
|
||||
posts_with_author_favorites = author.posts.find(:all, :include => :author_favorites)
|
||||
assert_no_queries { posts_with_author_favorites.first.author_favorites.first.author_id }
|
||||
end
|
||||
|
||||
def test_eager_with_has_many_through_an_sti_join_model
|
||||
author = Author.find(:first, :include => :special_post_comments, :order => 'authors.id')
|
||||
assert_equal [comments(:does_it_hurt)], assert_no_queries { author.special_post_comments }
|
||||
|
|
|
@ -3,6 +3,7 @@ require 'models/post'
|
|||
require 'models/comment'
|
||||
require 'models/project'
|
||||
require 'models/developer'
|
||||
require 'models/company_in_module'
|
||||
|
||||
class AssociationsExtensionsTest < ActiveRecord::TestCase
|
||||
fixtures :projects, :developers, :developers_projects, :comments, :posts
|
||||
|
@ -44,4 +45,18 @@ class AssociationsExtensionsTest < ActiveRecord::TestCase
|
|||
david = Marshal.load(Marshal.dump(david))
|
||||
assert_equal projects(:action_controller), david.projects_extended_by_name.find_most_recent
|
||||
end
|
||||
|
||||
|
||||
def test_extension_name
|
||||
extension = Proc.new {}
|
||||
name = :association_name
|
||||
|
||||
assert_equal 'DeveloperAssociationNameAssociationExtension', Developer.send(:create_extension_modules, name, extension, []).first.name
|
||||
assert_equal 'MyApplication::Business::DeveloperAssociationNameAssociationExtension',
|
||||
MyApplication::Business::Developer.send(:create_extension_modules, name, extension, []).first.name
|
||||
assert_equal 'MyApplication::Business::DeveloperAssociationNameAssociationExtension', MyApplication::Business::Developer.send(:create_extension_modules, name, extension, []).first.name
|
||||
assert_equal 'MyApplication::Business::DeveloperAssociationNameAssociationExtension', MyApplication::Business::Developer.send(:create_extension_modules, name, extension, []).first.name
|
||||
end
|
||||
|
||||
|
||||
end
|
||||
|
|
|
@ -223,10 +223,10 @@ class HasAndBelongsToManyAssociationsTest < ActiveRecord::TestCase
|
|||
devel = Developer.find(1)
|
||||
proj = assert_no_queries { devel.projects.build("name" => "Projekt") }
|
||||
assert !devel.projects.loaded?
|
||||
|
||||
|
||||
assert_equal devel.projects.last, proj
|
||||
assert devel.projects.loaded?
|
||||
|
||||
|
||||
assert proj.new_record?
|
||||
devel.save
|
||||
assert !proj.new_record?
|
||||
|
@ -251,10 +251,10 @@ class HasAndBelongsToManyAssociationsTest < ActiveRecord::TestCase
|
|||
devel = Developer.find(1)
|
||||
proj = devel.projects.create("name" => "Projekt")
|
||||
assert !devel.projects.loaded?
|
||||
|
||||
|
||||
assert_equal devel.projects.last, proj
|
||||
assert devel.projects.loaded?
|
||||
|
||||
assert !devel.projects.loaded?
|
||||
|
||||
assert !proj.new_record?
|
||||
assert_equal Developer.find(1).projects.sort_by(&:id).last, proj # prove join table is updated
|
||||
end
|
||||
|
@ -274,10 +274,10 @@ class HasAndBelongsToManyAssociationsTest < ActiveRecord::TestCase
|
|||
|
||||
def test_creation_respects_hash_condition
|
||||
post = categories(:general).post_with_conditions.build(:body => '')
|
||||
|
||||
|
||||
assert post.save
|
||||
assert_equal 'Yet Another Testing Title', post.title
|
||||
|
||||
|
||||
another_post = categories(:general).post_with_conditions.create(:body => '')
|
||||
|
||||
assert !another_post.new_record?
|
||||
|
@ -288,7 +288,7 @@ class HasAndBelongsToManyAssociationsTest < ActiveRecord::TestCase
|
|||
dev = developers(:jamis)
|
||||
dev.projects << projects(:active_record)
|
||||
dev.projects << projects(:active_record)
|
||||
|
||||
|
||||
assert_equal 3, dev.projects.size
|
||||
assert_equal 1, dev.projects.uniq.size
|
||||
end
|
||||
|
@ -415,13 +415,13 @@ class HasAndBelongsToManyAssociationsTest < ActiveRecord::TestCase
|
|||
project.developers.class # force load target
|
||||
|
||||
developer = project.developers.first
|
||||
|
||||
|
||||
assert_no_queries do
|
||||
assert project.developers.loaded?
|
||||
assert project.developers.include?(developer)
|
||||
end
|
||||
end
|
||||
|
||||
|
||||
def test_include_checks_if_record_exists_if_target_not_loaded
|
||||
project = projects(:active_record)
|
||||
developer = project.developers.first
|
||||
|
@ -636,11 +636,39 @@ class HasAndBelongsToManyAssociationsTest < ActiveRecord::TestCase
|
|||
assert_equal 3, Developer.find(:all, :include => {:projects => :developers}, :conditions => 'developers_projects_join.joined_on IS NOT NULL', :group => group.join(",")).size
|
||||
end
|
||||
|
||||
def test_find_grouped
|
||||
all_posts_from_category1 = Post.find(:all, :conditions => "category_id = 1", :joins => :categories)
|
||||
grouped_posts_of_category1 = Post.find(:all, :conditions => "category_id = 1", :group => "author_id", :select => 'count(posts.id) as posts_count', :joins => :categories)
|
||||
assert_equal 4, all_posts_from_category1.size
|
||||
assert_equal 1, grouped_posts_of_category1.size
|
||||
end
|
||||
|
||||
def test_find_scoped_grouped
|
||||
assert_equal 4, categories(:general).posts_gruoped_by_title.size
|
||||
assert_equal 1, categories(:technology).posts_gruoped_by_title.size
|
||||
end
|
||||
|
||||
def test_get_ids
|
||||
assert_equal projects(:active_record, :action_controller).map(&:id).sort, developers(:david).project_ids.sort
|
||||
assert_equal [projects(:active_record).id], developers(:jamis).project_ids
|
||||
end
|
||||
|
||||
def test_get_ids_for_loaded_associations
|
||||
developer = developers(:david)
|
||||
developer.projects(true)
|
||||
assert_queries(0) do
|
||||
developer.project_ids
|
||||
developer.project_ids
|
||||
end
|
||||
end
|
||||
|
||||
def test_get_ids_for_unloaded_associations_does_not_load_them
|
||||
developer = developers(:david)
|
||||
assert !developer.projects.loaded?
|
||||
assert_equal projects(:active_record, :action_controller).map(&:id).sort, developer.project_ids.sort
|
||||
assert !developer.projects.loaded?
|
||||
end
|
||||
|
||||
def test_assign_ids
|
||||
developer = Developer.new("name" => "Joe")
|
||||
developer.project_ids = projects(:active_record, :action_controller).map(&:id)
|
||||
|
@ -703,4 +731,20 @@ class HasAndBelongsToManyAssociationsTest < ActiveRecord::TestCase
|
|||
# due to Unknown column 'authors.id'
|
||||
assert Category.find(1).posts_with_authors_sorted_by_author_id.find_by_title('Welcome to the weblog')
|
||||
end
|
||||
|
||||
def test_counting_on_habtm_association_and_not_array
|
||||
david = Developer.find(1)
|
||||
# Extra parameter just to make sure we aren't falling back to
|
||||
# Array#count in Ruby >=1.8.7, which would raise an ArgumentError
|
||||
assert_nothing_raised { david.projects.count(:all, :conditions => '1=1') }
|
||||
end
|
||||
|
||||
uses_mocha 'mocking Post.transaction' do
|
||||
def test_association_proxy_transaction_method_starts_transaction_in_association_class
|
||||
Post.expects(:transaction)
|
||||
Category.find(:first).posts.transaction do
|
||||
# nothing
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
|
|
@ -135,6 +135,10 @@ class HasManyAssociationsTest < ActiveRecord::TestCase
|
|||
assert_equal "Microsoft", Firm.find(:first).clients_like_ms_with_hash_conditions.first.name
|
||||
end
|
||||
|
||||
def test_finding_using_primary_key
|
||||
assert_equal "Summit", Firm.find(:first).clients_using_primary_key.first.name
|
||||
end
|
||||
|
||||
def test_finding_using_sql
|
||||
firm = Firm.find(:first)
|
||||
first_client = firm.clients_using_sql.first
|
||||
|
@ -244,6 +248,13 @@ class HasManyAssociationsTest < ActiveRecord::TestCase
|
|||
assert_equal 1, grouped_clients_of_firm1.size
|
||||
end
|
||||
|
||||
def test_find_scoped_grouped
|
||||
assert_equal 1, companies(:first_firm).clients_grouped_by_firm_id.size
|
||||
assert_equal 1, companies(:first_firm).clients_grouped_by_firm_id.length
|
||||
assert_equal 2, companies(:first_firm).clients_grouped_by_name.size
|
||||
assert_equal 2, companies(:first_firm).clients_grouped_by_name.length
|
||||
end
|
||||
|
||||
def test_adding
|
||||
force_signal37_to_load_all_clients_of_firm
|
||||
natural = Client.new("name" => "Natural Company")
|
||||
|
@ -380,7 +391,7 @@ class HasManyAssociationsTest < ActiveRecord::TestCase
|
|||
company = companies(:first_firm)
|
||||
new_client = assert_no_queries { company.clients_of_firm.build("name" => "Another Client") }
|
||||
assert !company.clients_of_firm.loaded?
|
||||
|
||||
|
||||
assert_equal "Another Client", new_client.name
|
||||
assert new_client.new_record?
|
||||
assert_equal new_client, company.clients_of_firm.last
|
||||
|
@ -412,7 +423,7 @@ class HasManyAssociationsTest < ActiveRecord::TestCase
|
|||
def test_build_many
|
||||
company = companies(:first_firm)
|
||||
new_clients = assert_no_queries { company.clients_of_firm.build([{"name" => "Another Client"}, {"name" => "Another Client II"}]) }
|
||||
|
||||
|
||||
assert_equal 2, new_clients.size
|
||||
company.name += '-changed'
|
||||
assert_queries(3) { assert company.save }
|
||||
|
@ -651,10 +662,10 @@ class HasManyAssociationsTest < ActiveRecord::TestCase
|
|||
|
||||
def test_creation_respects_hash_condition
|
||||
ms_client = companies(:first_firm).clients_like_ms_with_hash_conditions.build
|
||||
|
||||
|
||||
assert ms_client.save
|
||||
assert_equal 'Microsoft', ms_client.name
|
||||
|
||||
|
||||
another_ms_client = companies(:first_firm).clients_like_ms_with_hash_conditions.create
|
||||
|
||||
assert !another_ms_client.new_record?
|
||||
|
@ -826,6 +837,29 @@ class HasManyAssociationsTest < ActiveRecord::TestCase
|
|||
assert_equal [companies(:first_client).id, companies(:second_client).id], companies(:first_firm).client_ids
|
||||
end
|
||||
|
||||
def test_get_ids_for_loaded_associations
|
||||
company = companies(:first_firm)
|
||||
company.clients(true)
|
||||
assert_queries(0) do
|
||||
company.client_ids
|
||||
company.client_ids
|
||||
end
|
||||
end
|
||||
|
||||
def test_get_ids_for_unloaded_associations_does_not_load_them
|
||||
company = companies(:first_firm)
|
||||
assert !company.clients.loaded?
|
||||
assert_equal [companies(:first_client).id, companies(:second_client).id], company.client_ids
|
||||
assert !company.clients.loaded?
|
||||
end
|
||||
|
||||
def test_get_ids_for_unloaded_finder_sql_associations_loads_them
|
||||
company = companies(:first_firm)
|
||||
assert !company.clients_using_sql.loaded?
|
||||
assert_equal [companies(:second_client).id], company.clients_using_sql_ids
|
||||
assert company.clients_using_sql.loaded?
|
||||
end
|
||||
|
||||
def test_assign_ids
|
||||
firm = Firm.new("name" => "Apple")
|
||||
firm.client_ids = [companies(:first_client).id, companies(:second_client).id]
|
||||
|
@ -896,7 +930,7 @@ class HasManyAssociationsTest < ActiveRecord::TestCase
|
|||
assert_equal 4, authors(:david).limited_comments.find(:all, :conditions => "comments.type = 'SpecialComment'", :limit => 9_000).length
|
||||
assert_equal 4, authors(:david).limited_comments.find_all_by_type('SpecialComment', :limit => 9_000).length
|
||||
end
|
||||
|
||||
|
||||
def test_find_all_include_over_the_same_table_for_through
|
||||
assert_equal 2, people(:michael).posts.find(:all, :include => :people).length
|
||||
end
|
||||
|
@ -933,13 +967,13 @@ class HasManyAssociationsTest < ActiveRecord::TestCase
|
|||
def test_include_loads_collection_if_target_uses_finder_sql
|
||||
firm = companies(:first_firm)
|
||||
client = firm.clients_using_sql.first
|
||||
|
||||
|
||||
firm.reload
|
||||
assert ! firm.clients_using_sql.loaded?
|
||||
assert firm.clients_using_sql.include?(client)
|
||||
assert firm.clients_using_sql.loaded?
|
||||
end
|
||||
|
||||
|
||||
|
||||
def test_include_returns_false_for_non_matching_record_to_verify_scoping
|
||||
firm = companies(:first_firm)
|
||||
|
@ -982,6 +1016,19 @@ class HasManyAssociationsTest < ActiveRecord::TestCase
|
|||
assert firm.clients.loaded?
|
||||
end
|
||||
|
||||
def test_calling_first_or_last_on_existing_record_with_create_should_not_load_association
|
||||
firm = companies(:first_firm)
|
||||
firm.clients.create(:name => 'Foo')
|
||||
assert !firm.clients.loaded?
|
||||
|
||||
assert_queries 2 do
|
||||
firm.clients.first
|
||||
firm.clients.last
|
||||
end
|
||||
|
||||
assert !firm.clients.loaded?
|
||||
end
|
||||
|
||||
def test_calling_first_or_last_on_new_record_should_not_run_queries
|
||||
firm = Firm.new
|
||||
|
||||
|
@ -1031,4 +1078,24 @@ class HasManyAssociationsTest < ActiveRecord::TestCase
|
|||
ActiveRecord::Base.store_full_sti_class = old
|
||||
end
|
||||
|
||||
uses_mocha 'mocking Comment.transaction' do
|
||||
def test_association_proxy_transaction_method_starts_transaction_in_association_class
|
||||
Comment.expects(:transaction)
|
||||
Post.find(:first).comments.transaction do
|
||||
# nothing
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
def test_sending_new_to_association_proxy_should_have_same_effect_as_calling_new
|
||||
client_association = companies(:first_firm).clients
|
||||
assert_equal client_association.new.attributes, client_association.send(:new).attributes
|
||||
end
|
||||
|
||||
def test_respond_to_private_class_methods
|
||||
client_association = companies(:first_firm).clients
|
||||
assert !client_association.respond_to?(:private_method)
|
||||
assert client_association.respond_to?(:private_method, true)
|
||||
end
|
||||
end
|
||||
|
||||
|
|
|
@ -2,15 +2,18 @@ require "cases/helper"
|
|||
require 'models/post'
|
||||
require 'models/person'
|
||||
require 'models/reader'
|
||||
require 'models/comment'
|
||||
|
||||
class HasManyThroughAssociationsTest < ActiveRecord::TestCase
|
||||
fixtures :posts, :readers, :people
|
||||
fixtures :posts, :readers, :people, :comments, :authors
|
||||
|
||||
def test_associate_existing
|
||||
assert_queries(2) { posts(:thinking);people(:david) }
|
||||
|
||||
|
||||
posts(:thinking).people
|
||||
|
||||
assert_queries(1) do
|
||||
posts(:thinking).people << people(:david)
|
||||
posts(:thinking).people << people(:david)
|
||||
end
|
||||
|
||||
assert_queries(1) do
|
||||
|
@ -197,4 +200,48 @@ class HasManyThroughAssociationsTest < ActiveRecord::TestCase
|
|||
def test_count_with_include_should_alias_join_table
|
||||
assert_equal 2, people(:michael).posts.count(:include => :readers)
|
||||
end
|
||||
|
||||
def test_get_ids
|
||||
assert_equal [posts(:welcome).id, posts(:authorless).id].sort, people(:michael).post_ids.sort
|
||||
end
|
||||
|
||||
def test_get_ids_for_loaded_associations
|
||||
person = people(:michael)
|
||||
person.posts(true)
|
||||
assert_queries(0) do
|
||||
person.post_ids
|
||||
person.post_ids
|
||||
end
|
||||
end
|
||||
|
||||
def test_get_ids_for_unloaded_associations_does_not_load_them
|
||||
person = people(:michael)
|
||||
assert !person.posts.loaded?
|
||||
assert_equal [posts(:welcome).id, posts(:authorless).id].sort, person.post_ids.sort
|
||||
assert !person.posts.loaded?
|
||||
end
|
||||
|
||||
uses_mocha 'mocking Tag.transaction' do
|
||||
def test_association_proxy_transaction_method_starts_transaction_in_association_class
|
||||
Tag.expects(:transaction)
|
||||
Post.find(:first).tags.transaction do
|
||||
# nothing
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
def test_has_many_association_through_a_belongs_to_association_where_the_association_doesnt_exist
|
||||
author = authors(:mary)
|
||||
post = Post.create!(:title => "TITLE", :body => "BODY")
|
||||
assert_equal [], post.author_favorites
|
||||
end
|
||||
|
||||
def test_has_many_association_through_a_belongs_to_association
|
||||
author = authors(:mary)
|
||||
post = Post.create!(:author => author, :title => "TITLE", :body => "BODY")
|
||||
author.author_favorites.create(:favorite_author_id => 1)
|
||||
author.author_favorites.create(:favorite_author_id => 2)
|
||||
author.author_favorites.create(:favorite_author_id => 3)
|
||||
assert_equal post.author.author_favorites, post.author_favorites
|
||||
end
|
||||
end
|
||||
|
|
|
@ -29,6 +29,13 @@ class HasOneAssociationsTest < ActiveRecord::TestCase
|
|||
assert_equal Firm.find(1, :include => :account_with_select).account_with_select.attributes.size, 2
|
||||
end
|
||||
|
||||
def test_finding_using_primary_key
|
||||
firm = companies(:first_firm)
|
||||
assert_equal Account.find_by_firm_id(firm.id), firm.account
|
||||
firm.firm_id = companies(:rails_core).id
|
||||
assert_equal accounts(:rails_core_account), firm.account_using_primary_key
|
||||
end
|
||||
|
||||
def test_can_marshal_has_one_association_with_nil_target
|
||||
firm = Firm.new
|
||||
assert_nothing_raised do
|
||||
|
@ -342,4 +349,14 @@ class HasOneAssociationsTest < ActiveRecord::TestCase
|
|||
assert companies(:first_firm).readonly_account.readonly?
|
||||
end
|
||||
|
||||
def test_has_one_proxy_should_not_respond_to_private_methods
|
||||
assert_raises(NoMethodError) { accounts(:signals37).private_method }
|
||||
assert_raises(NoMethodError) { companies(:first_firm).account.private_method }
|
||||
end
|
||||
|
||||
def test_has_one_proxy_should_respond_to_private_methods_via_send
|
||||
accounts(:signals37).send(:private_method)
|
||||
companies(:first_firm).account.send(:private_method)
|
||||
end
|
||||
|
||||
end
|
||||
|
|
|
@ -110,4 +110,14 @@ class HasOneThroughAssociationsTest < ActiveRecord::TestCase
|
|||
new_member.club = new_club = Club.create(:name => "LRUG")
|
||||
assert_equal new_club, new_member.club.target
|
||||
end
|
||||
|
||||
def test_has_one_through_proxy_should_not_respond_to_private_methods
|
||||
assert_raises(NoMethodError) { clubs(:moustache_club).private_method }
|
||||
assert_raises(NoMethodError) { @member.club.private_method }
|
||||
end
|
||||
|
||||
def test_has_one_through_proxy_should_respond_to_private_methods_via_send
|
||||
clubs(:moustache_club).send(:private_method)
|
||||
@member.club.send(:private_method)
|
||||
end
|
||||
end
|
||||
|
|
|
@ -1,5 +1,6 @@
|
|||
require "cases/helper"
|
||||
require 'models/topic'
|
||||
require 'models/minimalistic'
|
||||
|
||||
class AttributeMethodsTest < ActiveRecord::TestCase
|
||||
fixtures :topics
|
||||
|
@ -57,19 +58,19 @@ class AttributeMethodsTest < ActiveRecord::TestCase
|
|||
|
||||
def test_kernel_methods_not_implemented_in_activerecord
|
||||
%w(test name display y).each do |method|
|
||||
assert_equal false, ActiveRecord::Base.instance_method_already_implemented?(method), "##{method} is defined"
|
||||
assert !ActiveRecord::Base.instance_method_already_implemented?(method), "##{method} is defined"
|
||||
end
|
||||
end
|
||||
|
||||
def test_primary_key_implemented
|
||||
assert_equal true, Class.new(ActiveRecord::Base).instance_method_already_implemented?('id')
|
||||
assert Class.new(ActiveRecord::Base).instance_method_already_implemented?('id')
|
||||
end
|
||||
|
||||
def test_defined_kernel_methods_implemented_in_model
|
||||
%w(test name display y).each do |method|
|
||||
klass = Class.new ActiveRecord::Base
|
||||
klass.class_eval "def #{method}() 'defined #{method}' end"
|
||||
assert_equal true, klass.instance_method_already_implemented?(method), "##{method} is not defined"
|
||||
assert klass.instance_method_already_implemented?(method), "##{method} is not defined"
|
||||
end
|
||||
end
|
||||
|
||||
|
@ -79,7 +80,7 @@ class AttributeMethodsTest < ActiveRecord::TestCase
|
|||
abstract.class_eval "def #{method}() 'defined #{method}' end"
|
||||
abstract.abstract_class = true
|
||||
klass = Class.new abstract
|
||||
assert_equal true, klass.instance_method_already_implemented?(method), "##{method} is not defined"
|
||||
assert klass.instance_method_already_implemented?(method), "##{method} is not defined"
|
||||
end
|
||||
end
|
||||
|
||||
|
@ -219,6 +220,48 @@ class AttributeMethodsTest < ActiveRecord::TestCase
|
|||
end
|
||||
end
|
||||
|
||||
def test_setting_time_zone_conversion_for_attributes_should_write_value_on_class_variable
|
||||
Topic.skip_time_zone_conversion_for_attributes = [:field_a]
|
||||
Minimalistic.skip_time_zone_conversion_for_attributes = [:field_b]
|
||||
|
||||
assert_equal [:field_a], Topic.skip_time_zone_conversion_for_attributes
|
||||
assert_equal [:field_b], Minimalistic.skip_time_zone_conversion_for_attributes
|
||||
end
|
||||
|
||||
def test_read_attributes_respect_access_control
|
||||
privatize("title")
|
||||
|
||||
topic = @target.new(:title => "The pros and cons of programming naked.")
|
||||
assert !topic.respond_to?(:title)
|
||||
assert_raise(NoMethodError) { topic.title }
|
||||
topic.send(:title)
|
||||
end
|
||||
|
||||
def test_write_attributes_respect_access_control
|
||||
privatize("title=(value)")
|
||||
|
||||
topic = @target.new
|
||||
assert !topic.respond_to?(:title=)
|
||||
assert_raise(NoMethodError) { topic.title = "Pants"}
|
||||
topic.send(:title=, "Very large pants")
|
||||
end
|
||||
|
||||
def test_question_attributes_respect_access_control
|
||||
privatize("title?")
|
||||
|
||||
topic = @target.new(:title => "Isaac Newton's pants")
|
||||
assert !topic.respond_to?(:title?)
|
||||
assert_raise(NoMethodError) { topic.title? }
|
||||
assert topic.send(:title?)
|
||||
end
|
||||
|
||||
def test_bulk_update_respects_access_control
|
||||
privatize("title=(value)")
|
||||
|
||||
assert_raise(ActiveRecord::UnknownAttributeError) { topic = @target.new(:title => "Rants about pants") }
|
||||
assert_raise(ActiveRecord::UnknownAttributeError) { @target.new.attributes = { :title => "Ants in pants" } }
|
||||
end
|
||||
|
||||
private
|
||||
def time_related_columns_on_topic
|
||||
Topic.columns.select{|c| [:time, :date, :datetime, :timestamp].include?(c.type)}.map(&:name)
|
||||
|
@ -235,4 +278,13 @@ class AttributeMethodsTest < ActiveRecord::TestCase
|
|||
Time.zone = old_zone
|
||||
ActiveRecord::Base.time_zone_aware_attributes = old_tz
|
||||
end
|
||||
|
||||
def privatize(method_signature)
|
||||
@target.class_eval <<-private_method
|
||||
private
|
||||
def #{method_signature}
|
||||
"I'm private"
|
||||
end
|
||||
private_method
|
||||
end
|
||||
end
|
||||
|
|
113
vendor/rails/activerecord/test/cases/base_test.rb
vendored
113
vendor/rails/activerecord/test/cases/base_test.rb
vendored
|
@ -76,7 +76,7 @@ class TopicWithProtectedContentAndAccessibleAuthorName < ActiveRecord::Base
|
|||
end
|
||||
|
||||
class BasicsTest < ActiveRecord::TestCase
|
||||
fixtures :topics, :companies, :developers, :projects, :computers, :accounts, :minimalistics, 'warehouse-things', :authors, :categorizations, :categories
|
||||
fixtures :topics, :companies, :developers, :projects, :computers, :accounts, :minimalistics, 'warehouse-things', :authors, :categorizations, :categories, :posts
|
||||
|
||||
def test_table_exists
|
||||
assert !NonExistentTable.table_exists?
|
||||
|
@ -138,7 +138,7 @@ class BasicsTest < ActiveRecord::TestCase
|
|||
if current_adapter?(:MysqlAdapter)
|
||||
def test_read_attributes_before_type_cast_on_boolean
|
||||
bool = Booleantest.create({ "value" => false })
|
||||
assert_equal 0, bool.attributes_before_type_cast["value"]
|
||||
assert_equal "0", bool.reload.attributes_before_type_cast["value"]
|
||||
end
|
||||
end
|
||||
|
||||
|
@ -472,6 +472,18 @@ class BasicsTest < ActiveRecord::TestCase
|
|||
assert topic.instance_variable_get("@custom_approved")
|
||||
end
|
||||
|
||||
def test_delete
|
||||
topic = Topic.find(1)
|
||||
assert_equal topic, topic.delete, 'topic.delete did not return self'
|
||||
assert topic.frozen?, 'topic not frozen after delete'
|
||||
assert_raise(ActiveRecord::RecordNotFound) { Topic.find(topic.id) }
|
||||
end
|
||||
|
||||
def test_delete_doesnt_run_callbacks
|
||||
Topic.find(1).delete
|
||||
assert_not_nil Topic.find(2)
|
||||
end
|
||||
|
||||
def test_destroy
|
||||
topic = Topic.find(1)
|
||||
assert_equal topic, topic.destroy, 'topic.destroy did not return self'
|
||||
|
@ -664,10 +676,21 @@ class BasicsTest < ActiveRecord::TestCase
|
|||
end
|
||||
end
|
||||
|
||||
def test_update_all_ignores_order_limit_from_association
|
||||
author = Author.find(1)
|
||||
def test_update_all_ignores_order_without_limit_from_association
|
||||
author = authors(:david)
|
||||
assert_nothing_raised do
|
||||
assert_equal author.posts_with_comments_and_categories.length, author.posts_with_comments_and_categories.update_all("body = 'bulk update!'")
|
||||
assert_equal author.posts_with_comments_and_categories.length, author.posts_with_comments_and_categories.update_all([ "body = ?", "bulk update!" ])
|
||||
end
|
||||
end
|
||||
|
||||
def test_update_all_with_order_and_limit_updates_subset_only
|
||||
author = authors(:david)
|
||||
assert_nothing_raised do
|
||||
assert_equal 1, author.posts_sorted_by_id_limited.size
|
||||
assert_equal 2, author.posts_sorted_by_id_limited.find(:all, :limit => 2).size
|
||||
assert_equal 1, author.posts_sorted_by_id_limited.update_all([ "body = ?", "bulk update!" ])
|
||||
assert_equal "bulk update!", posts(:welcome).body
|
||||
assert_not_equal "bulk update!", posts(:thinking).body
|
||||
end
|
||||
end
|
||||
|
||||
|
@ -809,6 +832,20 @@ class BasicsTest < ActiveRecord::TestCase
|
|||
assert_equal [ Topic.find(1) ], [ Topic.find(2).topic ] & [ Topic.find(1) ]
|
||||
end
|
||||
|
||||
def test_delete_new_record
|
||||
client = Client.new
|
||||
client.delete
|
||||
assert client.frozen?
|
||||
end
|
||||
|
||||
def test_delete_record_with_associations
|
||||
client = Client.find(3)
|
||||
client.delete
|
||||
assert client.frozen?
|
||||
assert_kind_of Firm, client.firm
|
||||
assert_raises(ActiveSupport::FrozenObjectError) { client.name = "something else" }
|
||||
end
|
||||
|
||||
def test_destroy_new_record
|
||||
client = Client.new
|
||||
client.destroy
|
||||
|
@ -880,7 +917,7 @@ class BasicsTest < ActiveRecord::TestCase
|
|||
|
||||
def test_mass_assignment_protection_against_class_attribute_writers
|
||||
[:logger, :configurations, :primary_key_prefix_type, :table_name_prefix, :table_name_suffix, :pluralize_table_names, :colorize_logging,
|
||||
:default_timezone, :allow_concurrency, :schema_format, :verification_timeout, :lock_optimistically, :record_timestamps].each do |method|
|
||||
:default_timezone, :schema_format, :lock_optimistically, :record_timestamps].each do |method|
|
||||
assert Task.respond_to?(method)
|
||||
assert Task.respond_to?("#{method}=")
|
||||
assert Task.new.respond_to?(method)
|
||||
|
@ -904,6 +941,14 @@ class BasicsTest < ActiveRecord::TestCase
|
|||
assert_nil keyboard.id
|
||||
end
|
||||
|
||||
def test_mass_assigning_invalid_attribute
|
||||
firm = Firm.new
|
||||
|
||||
assert_raises(ActiveRecord::UnknownAttributeError) do
|
||||
firm.attributes = { "id" => 5, "type" => "Client", "i_dont_even_exist" => 20 }
|
||||
end
|
||||
end
|
||||
|
||||
def test_mass_assignment_protection_on_defaults
|
||||
firm = Firm.new
|
||||
firm.attributes = { "id" => 5, "type" => "Client" }
|
||||
|
@ -1065,6 +1110,24 @@ class BasicsTest < ActiveRecord::TestCase
|
|||
Time.zone = nil
|
||||
Topic.skip_time_zone_conversion_for_attributes = []
|
||||
end
|
||||
|
||||
def test_multiparameter_attributes_on_time_only_column_with_time_zone_aware_attributes_does_not_do_time_zone_conversion
|
||||
ActiveRecord::Base.time_zone_aware_attributes = true
|
||||
ActiveRecord::Base.default_timezone = :utc
|
||||
Time.zone = ActiveSupport::TimeZone[-28800]
|
||||
attributes = {
|
||||
"bonus_time(1i)" => "2000", "bonus_time(2i)" => "1", "bonus_time(3i)" => "1",
|
||||
"bonus_time(4i)" => "16", "bonus_time(5i)" => "24"
|
||||
}
|
||||
topic = Topic.find(1)
|
||||
topic.attributes = attributes
|
||||
assert_equal Time.utc(2000, 1, 1, 16, 24, 0), topic.bonus_time
|
||||
assert topic.bonus_time.utc?
|
||||
ensure
|
||||
ActiveRecord::Base.time_zone_aware_attributes = false
|
||||
ActiveRecord::Base.default_timezone = :local
|
||||
Time.zone = nil
|
||||
end
|
||||
|
||||
def test_multiparameter_attributes_on_time_with_empty_seconds
|
||||
attributes = {
|
||||
|
@ -1106,11 +1169,15 @@ class BasicsTest < ActiveRecord::TestCase
|
|||
end
|
||||
|
||||
def test_boolean
|
||||
b_nil = Booleantest.create({ "value" => nil })
|
||||
nil_id = b_nil.id
|
||||
b_false = Booleantest.create({ "value" => false })
|
||||
false_id = b_false.id
|
||||
b_true = Booleantest.create({ "value" => true })
|
||||
true_id = b_true.id
|
||||
|
||||
b_nil = Booleantest.find(nil_id)
|
||||
assert_nil b_nil.value
|
||||
b_false = Booleantest.find(false_id)
|
||||
assert !b_false.value?
|
||||
b_true = Booleantest.find(true_id)
|
||||
|
@ -1118,11 +1185,15 @@ class BasicsTest < ActiveRecord::TestCase
|
|||
end
|
||||
|
||||
def test_boolean_cast_from_string
|
||||
b_blank = Booleantest.create({ "value" => "" })
|
||||
blank_id = b_blank.id
|
||||
b_false = Booleantest.create({ "value" => "0" })
|
||||
false_id = b_false.id
|
||||
b_true = Booleantest.create({ "value" => "1" })
|
||||
true_id = b_true.id
|
||||
|
||||
b_blank = Booleantest.find(blank_id)
|
||||
assert_nil b_blank.value
|
||||
b_false = Booleantest.find(false_id)
|
||||
assert !b_false.value?
|
||||
b_true = Booleantest.find(true_id)
|
||||
|
@ -1393,15 +1464,17 @@ class BasicsTest < ActiveRecord::TestCase
|
|||
|
||||
if RUBY_VERSION < '1.9'
|
||||
def test_quote_chars
|
||||
str = 'The Narrator'
|
||||
topic = Topic.create(:author_name => str)
|
||||
assert_equal str, topic.author_name
|
||||
with_kcode('UTF8') do
|
||||
str = 'The Narrator'
|
||||
topic = Topic.create(:author_name => str)
|
||||
assert_equal str, topic.author_name
|
||||
|
||||
assert_kind_of ActiveSupport::Multibyte::Chars, str.chars
|
||||
topic = Topic.find_by_author_name(str.chars)
|
||||
assert_kind_of ActiveSupport::Multibyte.proxy_class, str.mb_chars
|
||||
topic = Topic.find_by_author_name(str.mb_chars)
|
||||
|
||||
assert_kind_of Topic, topic
|
||||
assert_equal str, topic.author_name, "The right topic should have been found by name even with name passed as Chars"
|
||||
assert_kind_of Topic, topic
|
||||
assert_equal str, topic.author_name, "The right topic should have been found by name even with name passed as Chars"
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
|
@ -1994,4 +2067,18 @@ class BasicsTest < ActiveRecord::TestCase
|
|||
ensure
|
||||
ActiveRecord::Base.logger = original_logger
|
||||
end
|
||||
|
||||
private
|
||||
def with_kcode(kcode)
|
||||
if RUBY_VERSION < '1.9'
|
||||
orig_kcode, $KCODE = $KCODE, kcode
|
||||
begin
|
||||
yield
|
||||
ensure
|
||||
$KCODE = orig_kcode
|
||||
end
|
||||
else
|
||||
yield
|
||||
end
|
||||
end
|
||||
end
|
||||
|
|
|
@ -18,8 +18,8 @@ class CalculationsTest < ActiveRecord::TestCase
|
|||
|
||||
def test_should_average_field
|
||||
value = Account.average(:credit_limit)
|
||||
assert_kind_of Float, value
|
||||
assert_in_delta 53.0, value, 0.001
|
||||
assert_kind_of BigDecimal, value
|
||||
assert_equal BigDecimal.new('53.0'), value
|
||||
end
|
||||
|
||||
def test_should_return_nil_as_average
|
||||
|
@ -273,7 +273,7 @@ class CalculationsTest < ActiveRecord::TestCase
|
|||
end
|
||||
|
||||
def test_should_sum_expression
|
||||
assert_equal 636, Account.sum("2 * credit_limit")
|
||||
assert_equal '636', Account.sum("2 * credit_limit")
|
||||
end
|
||||
|
||||
def test_count_with_from_option
|
||||
|
|
38
vendor/rails/activerecord/test/cases/callbacks_observers_test.rb
vendored
Normal file
38
vendor/rails/activerecord/test/cases/callbacks_observers_test.rb
vendored
Normal file
|
@ -0,0 +1,38 @@
|
|||
require "cases/helper"
|
||||
|
||||
class Comment < ActiveRecord::Base
|
||||
attr_accessor :callers
|
||||
|
||||
before_validation :record_callers
|
||||
|
||||
def after_validation
|
||||
record_callers
|
||||
end
|
||||
|
||||
def record_callers
|
||||
callers << self.class if callers
|
||||
end
|
||||
end
|
||||
|
||||
class CommentObserver < ActiveRecord::Observer
|
||||
attr_accessor :callers
|
||||
|
||||
def after_validation(model)
|
||||
callers << self.class if callers
|
||||
end
|
||||
end
|
||||
|
||||
class CallbacksObserversTest < ActiveRecord::TestCase
|
||||
def test_model_callbacks_fire_before_observers_are_notified
|
||||
callers = []
|
||||
|
||||
comment = Comment.new
|
||||
comment.callers = callers
|
||||
|
||||
CommentObserver.instance.callers = callers
|
||||
|
||||
comment.valid?
|
||||
|
||||
assert_equal [Comment, Comment, CommentObserver], callers, "model callbacks did not fire before observers were notified"
|
||||
end
|
||||
end
|
|
@ -24,7 +24,7 @@ class MysqlConnectionTest < ActiveRecord::TestCase
|
|||
assert @connection.active?
|
||||
@connection.update('set @@wait_timeout=1')
|
||||
sleep 2
|
||||
@connection.verify!(0)
|
||||
@connection.verify!
|
||||
assert @connection.active?
|
||||
end
|
||||
end
|
||||
|
|
|
@ -19,6 +19,37 @@ class DefaultTest < ActiveRecord::TestCase
|
|||
end
|
||||
|
||||
if current_adapter?(:MysqlAdapter)
|
||||
|
||||
#MySQL 5 and higher is quirky with not null text/blob columns.
|
||||
#With MySQL Text/blob columns cannot have defaults. If the column is not null MySQL will report that the column has a null default
|
||||
#but it behaves as though the column had a default of ''
|
||||
def test_mysql_text_not_null_defaults
|
||||
klass = Class.new(ActiveRecord::Base)
|
||||
klass.table_name = 'test_mysql_text_not_null_defaults'
|
||||
klass.connection.create_table klass.table_name do |t|
|
||||
t.column :non_null_text, :text, :null => false
|
||||
t.column :non_null_blob, :blob, :null => false
|
||||
t.column :null_text, :text, :null => true
|
||||
t.column :null_blob, :blob, :null => true
|
||||
end
|
||||
assert_equal '', klass.columns_hash['non_null_blob'].default
|
||||
assert_equal '', klass.columns_hash['non_null_text'].default
|
||||
|
||||
assert_equal nil, klass.columns_hash['null_blob'].default
|
||||
assert_equal nil, klass.columns_hash['null_text'].default
|
||||
|
||||
assert_nothing_raised do
|
||||
instance = klass.create!
|
||||
assert_equal '', instance.non_null_text
|
||||
assert_equal '', instance.non_null_blob
|
||||
assert_nil instance.null_text
|
||||
assert_nil instance.null_blob
|
||||
end
|
||||
ensure
|
||||
klass.connection.drop_table(klass.table_name) rescue nil
|
||||
end
|
||||
|
||||
|
||||
# MySQL uses an implicit default 0 rather than NULL unless in strict mode.
|
||||
# We use an implicit NULL so schema.rb is compatible with other databases.
|
||||
def test_mysql_integer_not_null_defaults
|
||||
|
|
|
@ -45,6 +45,19 @@ class DirtyTest < ActiveRecord::TestCase
|
|||
assert_nil pirate.catchphrase_change
|
||||
end
|
||||
|
||||
def test_aliased_attribute_changes
|
||||
# the actual attribute here is name, title is an
|
||||
# alias setup via alias_attribute
|
||||
parrot = Parrot.new
|
||||
assert !parrot.title_changed?
|
||||
assert_nil parrot.title_change
|
||||
|
||||
parrot.name = 'Sam'
|
||||
assert parrot.title_changed?
|
||||
assert_nil parrot.title_was
|
||||
assert_equal parrot.name_change, parrot.title_change
|
||||
end
|
||||
|
||||
def test_nullable_integer_not_marked_as_changed_if_new_value_is_blank
|
||||
pirate = Pirate.new
|
||||
|
||||
|
|
159
vendor/rails/activerecord/test/cases/finder_test.rb
vendored
159
vendor/rails/activerecord/test/cases/finder_test.rb
vendored
|
@ -12,6 +12,57 @@ require 'models/customer'
|
|||
require 'models/job'
|
||||
require 'models/categorization'
|
||||
|
||||
class DynamicFinderMatchTest < ActiveRecord::TestCase
|
||||
def test_find_no_match
|
||||
assert_nil ActiveRecord::DynamicFinderMatch.match("not_a_finder")
|
||||
end
|
||||
|
||||
def test_find_by
|
||||
match = ActiveRecord::DynamicFinderMatch.match("find_by_age_and_sex_and_location")
|
||||
assert_not_nil match
|
||||
assert match.finder?
|
||||
assert_equal :first, match.finder
|
||||
assert_equal %w(age sex location), match.attribute_names
|
||||
end
|
||||
|
||||
def find_by_bang
|
||||
match = ActiveRecord::DynamicFinderMatch.match("find_by_age_and_sex_and_location!")
|
||||
assert_not_nil match
|
||||
assert match.finder?
|
||||
assert match.bang?
|
||||
assert_equal :first, match.finder
|
||||
assert_equal %w(age sex location), match.attribute_names
|
||||
end
|
||||
|
||||
def test_find_all_by
|
||||
match = ActiveRecord::DynamicFinderMatch.match("find_all_by_age_and_sex_and_location")
|
||||
assert_not_nil match
|
||||
assert match.finder?
|
||||
assert_equal :all, match.finder
|
||||
assert_equal %w(age sex location), match.attribute_names
|
||||
end
|
||||
|
||||
def test_find_or_initialize_by
|
||||
match = ActiveRecord::DynamicFinderMatch.match("find_or_initialize_by_age_and_sex_and_location")
|
||||
assert_not_nil match
|
||||
assert !match.finder?
|
||||
assert match.instantiator?
|
||||
assert_equal :first, match.finder
|
||||
assert_equal :new, match.instantiator
|
||||
assert_equal %w(age sex location), match.attribute_names
|
||||
end
|
||||
|
||||
def test_find_or_create_by
|
||||
match = ActiveRecord::DynamicFinderMatch.match("find_or_create_by_age_and_sex_and_location")
|
||||
assert_not_nil match
|
||||
assert !match.finder?
|
||||
assert match.instantiator?
|
||||
assert_equal :first, match.finder
|
||||
assert_equal :create, match.instantiator
|
||||
assert_equal %w(age sex location), match.attribute_names
|
||||
end
|
||||
end
|
||||
|
||||
class FinderTest < ActiveRecord::TestCase
|
||||
fixtures :companies, :topics, :entrants, :developers, :developers_projects, :posts, :comments, :accounts, :authors, :customers
|
||||
|
||||
|
@ -118,6 +169,12 @@ class FinderTest < ActiveRecord::TestCase
|
|||
assert_equal("fixture_3", developers.first.name)
|
||||
end
|
||||
|
||||
def test_find_with_group
|
||||
developers = Developer.find(:all, :group => "salary", :select => "salary")
|
||||
assert_equal 4, developers.size
|
||||
assert_equal 4, developers.map(&:salary).uniq.size
|
||||
end
|
||||
|
||||
def test_find_with_entire_select_statement
|
||||
topics = Topic.find_by_sql "SELECT * FROM topics WHERE author_name = 'Mary'"
|
||||
|
||||
|
@ -146,11 +203,11 @@ class FinderTest < ActiveRecord::TestCase
|
|||
first = Topic.find(:first, :conditions => "title = 'The First Topic!'")
|
||||
assert_nil(first)
|
||||
end
|
||||
|
||||
|
||||
def test_first
|
||||
assert_equal topics(:second).title, Topic.first(:conditions => "title = 'The Second Topic of the day'").title
|
||||
end
|
||||
|
||||
|
||||
def test_first_failing
|
||||
assert_nil Topic.first(:conditions => "title = 'The Second Topic of the day!'")
|
||||
end
|
||||
|
@ -200,6 +257,23 @@ class FinderTest < ActiveRecord::TestCase
|
|||
assert_raises(ActiveRecord::RecordNotFound) { Topic.find(1, :conditions => { 'topics.approved' => true }) }
|
||||
end
|
||||
|
||||
def test_find_on_hash_conditions_with_hashed_table_name
|
||||
assert Topic.find(1, :conditions => {:topics => { :approved => false }})
|
||||
assert_raises(ActiveRecord::RecordNotFound) { Topic.find(1, :conditions => {:topics => { :approved => true }}) }
|
||||
end
|
||||
|
||||
def test_find_with_hash_conditions_on_joined_table
|
||||
firms = Firm.all :joins => :account, :conditions => {:accounts => { :credit_limit => 50 }}
|
||||
assert_equal 1, firms.size
|
||||
assert_equal companies(:first_firm), firms.first
|
||||
end
|
||||
|
||||
def test_find_with_hash_conditions_on_joined_table_and_with_range
|
||||
firms = DependentFirm.all :joins => :account, :conditions => {:name => 'RailsCore', :accounts => { :credit_limit => 55..60 }}
|
||||
assert_equal 1, firms.size
|
||||
assert_equal companies(:rails_core), firms.first
|
||||
end
|
||||
|
||||
def test_find_on_hash_conditions_with_explicit_table_name_and_aggregate
|
||||
david = customers(:david)
|
||||
assert Customer.find(david.id, :conditions => { 'customers.name' => david.name, :address => david.address })
|
||||
|
@ -229,7 +303,6 @@ class FinderTest < ActiveRecord::TestCase
|
|||
assert_raises(ActiveRecord::RecordNotFound) { Topic.find(1, :conditions => { :author_name => "David", :title => "The First Topic", :replies_count => 1, :approved => true }) }
|
||||
end
|
||||
|
||||
|
||||
def test_condition_interpolation
|
||||
assert_kind_of Firm, Company.find(:first, :conditions => ["name = '%s'", "37signals"])
|
||||
assert_nil Company.find(:first, :conditions => ["name = '%s'", "37signals!"])
|
||||
|
@ -324,7 +397,7 @@ class FinderTest < ActiveRecord::TestCase
|
|||
Company.find(:first, :conditions => ["id=? AND name = ?", 2])
|
||||
}
|
||||
assert_raises(ActiveRecord::PreparedStatementInvalid) {
|
||||
Company.find(:first, :conditions => ["id=?", 2, 3, 4])
|
||||
Company.find(:first, :conditions => ["id=?", 2, 3, 4])
|
||||
}
|
||||
end
|
||||
|
||||
|
@ -350,7 +423,7 @@ class FinderTest < ActiveRecord::TestCase
|
|||
def test_named_bind_variables
|
||||
assert_equal '1', bind(':a', :a => 1) # ' ruby-mode
|
||||
assert_equal '1 1', bind(':a :a', :a => 1) # ' ruby-mode
|
||||
|
||||
|
||||
assert_nothing_raised { bind("'+00:00'", :foo => "bar") }
|
||||
|
||||
assert_kind_of Firm, Company.find(:first, :conditions => ["name = :name", { :name => "37signals" }])
|
||||
|
@ -387,6 +460,15 @@ class FinderTest < ActiveRecord::TestCase
|
|||
assert_equal ActiveRecord::Base.connection.quote(''), bind('?', '')
|
||||
end
|
||||
|
||||
def test_bind_chars
|
||||
quoted_bambi = ActiveRecord::Base.connection.quote("Bambi")
|
||||
quoted_bambi_and_thumper = ActiveRecord::Base.connection.quote("Bambi\nand\nThumper")
|
||||
assert_equal "name=#{quoted_bambi}", bind('name=?', "Bambi")
|
||||
assert_equal "name=#{quoted_bambi_and_thumper}", bind('name=?', "Bambi\nand\nThumper")
|
||||
assert_equal "name=#{quoted_bambi}", bind('name=?', "Bambi".mb_chars)
|
||||
assert_equal "name=#{quoted_bambi_and_thumper}", bind('name=?', "Bambi\nand\nThumper".mb_chars)
|
||||
end
|
||||
|
||||
def test_bind_record
|
||||
o = Struct.new(:quoted_id).new(1)
|
||||
assert_equal '1', bind('?', o)
|
||||
|
@ -418,11 +500,33 @@ class FinderTest < ActiveRecord::TestCase
|
|||
assert_equal(2, Entrant.count_by_sql(["SELECT COUNT(*) FROM entrants WHERE id > ?", 1]))
|
||||
end
|
||||
|
||||
uses_mocha('test_dynamic_finder_should_go_through_the_find_class_method') do
|
||||
def test_dynamic_finders_should_go_through_the_find_class_method
|
||||
Topic.expects(:find).with(:first, :conditions => { :title => 'The First Topic!' })
|
||||
Topic.find_by_title("The First Topic!")
|
||||
|
||||
Topic.expects(:find).with(:last, :conditions => { :title => 'The Last Topic!' })
|
||||
Topic.find_last_by_title("The Last Topic!")
|
||||
|
||||
Topic.expects(:find).with(:all, :conditions => { :title => 'A Topic.' })
|
||||
Topic.find_all_by_title("A Topic.")
|
||||
|
||||
Topic.expects(:find).with(:first, :conditions => { :title => 'Does not exist yet for sure!' }).times(2)
|
||||
Topic.find_or_initialize_by_title('Does not exist yet for sure!')
|
||||
Topic.find_or_create_by_title('Does not exist yet for sure!')
|
||||
end
|
||||
end
|
||||
|
||||
def test_find_by_one_attribute
|
||||
assert_equal topics(:first), Topic.find_by_title("The First Topic")
|
||||
assert_nil Topic.find_by_title("The First Topic!")
|
||||
end
|
||||
|
||||
def test_find_by_one_attribute_bang
|
||||
assert_equal topics(:first), Topic.find_by_title!("The First Topic")
|
||||
assert_raises(ActiveRecord::RecordNotFound) { Topic.find_by_title!("The First Topic!") }
|
||||
end
|
||||
|
||||
def test_find_by_one_attribute_caches_dynamic_finder
|
||||
# ensure this test can run independently of order
|
||||
class << Topic; self; end.send(:remove_method, :find_by_title) if Topic.public_methods.any? { |m| m.to_s == 'find_by_title' }
|
||||
|
@ -516,6 +620,38 @@ class FinderTest < ActiveRecord::TestCase
|
|||
assert_nil Topic.find_by_title_and_author_name("The First Topic", "Mary")
|
||||
end
|
||||
|
||||
def test_find_last_by_one_attribute
|
||||
assert_equal Topic.last, Topic.find_last_by_title(Topic.last.title)
|
||||
assert_nil Topic.find_last_by_title("A title with no matches")
|
||||
end
|
||||
|
||||
def test_find_last_by_one_attribute_caches_dynamic_finder
|
||||
# ensure this test can run independently of order
|
||||
class << Topic; self; end.send(:remove_method, :find_last_by_title) if Topic.public_methods.any? { |m| m.to_s == 'find_last_by_title' }
|
||||
assert !Topic.public_methods.any? { |m| m.to_s == 'find_last_by_title' }
|
||||
t = Topic.find_last_by_title(Topic.last.title)
|
||||
assert Topic.public_methods.any? { |m| m.to_s == 'find_last_by_title' }
|
||||
end
|
||||
|
||||
def test_find_last_by_invalid_method_syntax
|
||||
assert_raises(NoMethodError) { Topic.fail_to_find_last_by_title("The First Topic") }
|
||||
assert_raises(NoMethodError) { Topic.find_last_by_title?("The First Topic") }
|
||||
end
|
||||
|
||||
def test_find_last_by_one_attribute_with_several_options
|
||||
assert_equal accounts(:signals37), Account.find_last_by_credit_limit(50, :order => 'id DESC', :conditions => ['id != ?', 3])
|
||||
end
|
||||
|
||||
def test_find_last_by_one_missing_attribute
|
||||
assert_raises(NoMethodError) { Topic.find_last_by_undertitle("The Last Topic!") }
|
||||
end
|
||||
|
||||
def test_find_last_by_two_attributes
|
||||
topic = Topic.last
|
||||
assert_equal topic, Topic.find_last_by_title_and_author_name(topic.title, topic.author_name)
|
||||
assert_nil Topic.find_last_by_title_and_author_name(topic.title, "Anonymous")
|
||||
end
|
||||
|
||||
def test_find_all_by_one_attribute
|
||||
topics = Topic.find_all_by_content("Have a nice day")
|
||||
assert_equal 2, topics.size
|
||||
|
@ -709,7 +845,7 @@ class FinderTest < ActiveRecord::TestCase
|
|||
assert c.valid?
|
||||
assert !c.new_record?
|
||||
end
|
||||
|
||||
|
||||
def test_dynamic_find_or_initialize_from_one_attribute_caches_method
|
||||
class << Company; self; end.send(:remove_method, :find_or_initialize_by_name) if Company.public_methods.any? { |m| m.to_s == 'find_or_initialize_by_name' }
|
||||
assert !Company.public_methods.any? { |m| m.to_s == 'find_or_initialize_by_name' }
|
||||
|
@ -816,6 +952,17 @@ class FinderTest < ActiveRecord::TestCase
|
|||
assert_equal 1, first.id
|
||||
end
|
||||
|
||||
def test_joins_with_string_array
|
||||
person_with_reader_and_post = Post.find(
|
||||
:all,
|
||||
:joins => [
|
||||
"INNER JOIN categorizations ON categorizations.post_id = posts.id",
|
||||
"INNER JOIN categories ON categories.id = categorizations.category_id AND categories.type = 'SpecialCategory'"
|
||||
]
|
||||
)
|
||||
assert_equal 1, person_with_reader_and_post.size
|
||||
end
|
||||
|
||||
def test_find_by_id_with_conditions_with_or
|
||||
assert_nothing_raised do
|
||||
Post.find([1,2,3],
|
||||
|
|
|
@ -15,6 +15,7 @@ require 'models/pirate'
|
|||
require 'models/treasure'
|
||||
require 'models/matey'
|
||||
require 'models/ship'
|
||||
require 'models/book'
|
||||
|
||||
class FixturesTest < ActiveRecord::TestCase
|
||||
self.use_instantiated_fixtures = true
|
||||
|
@ -373,6 +374,34 @@ class CheckSetTableNameFixturesTest < ActiveRecord::TestCase
|
|||
end
|
||||
end
|
||||
|
||||
class FixtureNameIsNotTableNameFixturesTest < ActiveRecord::TestCase
|
||||
set_fixture_class :items => Book
|
||||
fixtures :items
|
||||
# Set to false to blow away fixtures cache and ensure our fixtures are loaded
|
||||
# and thus takes into account our set_fixture_class
|
||||
self.use_transactional_fixtures = false
|
||||
|
||||
def test_named_accessor
|
||||
assert_kind_of Book, items(:dvd)
|
||||
end
|
||||
end
|
||||
|
||||
class FixtureNameIsNotTableNameMultipleFixturesTest < ActiveRecord::TestCase
|
||||
set_fixture_class :items => Book, :funny_jokes => Joke
|
||||
fixtures :items, :funny_jokes
|
||||
# Set to false to blow away fixtures cache and ensure our fixtures are loaded
|
||||
# and thus takes into account our set_fixture_class
|
||||
self.use_transactional_fixtures = false
|
||||
|
||||
def test_named_accessor_of_differently_named_fixture
|
||||
assert_kind_of Book, items(:dvd)
|
||||
end
|
||||
|
||||
def test_named_accessor_of_same_named_fixture
|
||||
assert_kind_of Joke, funny_jokes(:a_joke)
|
||||
end
|
||||
end
|
||||
|
||||
class CustomConnectionFixturesTest < ActiveRecord::TestCase
|
||||
set_fixture_class :courses => Course
|
||||
fixtures :courses
|
||||
|
@ -432,11 +461,11 @@ class FixturesBrokenRollbackTest < ActiveRecord::TestCase
|
|||
alias_method :teardown, :blank_teardown
|
||||
|
||||
def test_no_rollback_in_teardown_unless_transaction_active
|
||||
assert_equal 0, Thread.current['open_transactions']
|
||||
assert_equal 0, ActiveRecord::Base.connection.open_transactions
|
||||
assert_raise(RuntimeError) { ar_setup_fixtures }
|
||||
assert_equal 0, Thread.current['open_transactions']
|
||||
assert_equal 0, ActiveRecord::Base.connection.open_transactions
|
||||
assert_nothing_raised { ar_teardown_fixtures }
|
||||
assert_equal 0, Thread.current['open_transactions']
|
||||
assert_equal 0, ActiveRecord::Base.connection.open_transactions
|
||||
end
|
||||
|
||||
private
|
||||
|
|
15
vendor/rails/activerecord/test/cases/helper.rb
vendored
15
vendor/rails/activerecord/test/cases/helper.rb
vendored
|
@ -1,4 +1,5 @@
|
|||
$:.unshift(File.dirname(__FILE__) + '/../../lib')
|
||||
$:.unshift(File.dirname(__FILE__) + '/../../../activesupport/lib')
|
||||
|
||||
require 'config'
|
||||
require 'test/unit'
|
||||
|
@ -45,3 +46,17 @@ end
|
|||
class << ActiveRecord::Base
|
||||
public :with_scope, :with_exclusive_scope
|
||||
end
|
||||
|
||||
unless ENV['FIXTURE_DEBUG']
|
||||
module Test #:nodoc:
|
||||
module Unit #:nodoc:
|
||||
class << TestCase #:nodoc:
|
||||
def try_to_load_dependency_with_silence(*args)
|
||||
ActiveRecord::Base.logger.silence { try_to_load_dependency_without_silence(*args)}
|
||||
end
|
||||
|
||||
alias_method_chain :try_to_load_dependency, :silence
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
41
vendor/rails/activerecord/test/cases/i18n_test.rb
vendored
Normal file
41
vendor/rails/activerecord/test/cases/i18n_test.rb
vendored
Normal file
|
@ -0,0 +1,41 @@
|
|||
require "cases/helper"
|
||||
require 'models/topic'
|
||||
require 'models/reply'
|
||||
|
||||
class ActiveRecordI18nTests < Test::Unit::TestCase
|
||||
|
||||
def setup
|
||||
I18n.backend = I18n::Backend::Simple.new
|
||||
end
|
||||
|
||||
def test_translated_model_attributes
|
||||
I18n.backend.store_translations 'en-US', :activerecord => {:attributes => {:topic => {:title => 'topic title attribute'} } }
|
||||
assert_equal 'topic title attribute', Topic.human_attribute_name('title')
|
||||
end
|
||||
|
||||
def test_translated_model_attributes_with_sti
|
||||
I18n.backend.store_translations 'en-US', :activerecord => {:attributes => {:reply => {:title => 'reply title attribute'} } }
|
||||
assert_equal 'reply title attribute', Reply.human_attribute_name('title')
|
||||
end
|
||||
|
||||
def test_translated_model_attributes_with_sti_fallback
|
||||
I18n.backend.store_translations 'en-US', :activerecord => {:attributes => {:topic => {:title => 'topic title attribute'} } }
|
||||
assert_equal 'topic title attribute', Reply.human_attribute_name('title')
|
||||
end
|
||||
|
||||
def test_translated_model_names
|
||||
I18n.backend.store_translations 'en-US', :activerecord => {:models => {:topic => 'topic model'} }
|
||||
assert_equal 'topic model', Topic.human_name
|
||||
end
|
||||
|
||||
def test_translated_model_names_with_sti
|
||||
I18n.backend.store_translations 'en-US', :activerecord => {:models => {:reply => 'reply model'} }
|
||||
assert_equal 'reply model', Reply.human_name
|
||||
end
|
||||
|
||||
def test_translated_model_names_with_sti_fallback
|
||||
I18n.backend.store_translations 'en-US', :activerecord => {:models => {:topic => 'topic model'} }
|
||||
assert_equal 'topic model', Reply.human_name
|
||||
end
|
||||
end
|
||||
|
|
@ -193,7 +193,7 @@ class InheritanceTest < ActiveRecord::TestCase
|
|||
|
||||
def test_eager_load_belongs_to_primary_key_quoting
|
||||
con = Account.connection
|
||||
assert_sql(/\(#{con.quote_table_name('companies')}.#{con.quote_column_name('id')} IN \(1\)\)/) do
|
||||
assert_sql(/\(#{con.quote_table_name('companies')}.#{con.quote_column_name('id')} = 1\)/) do
|
||||
Account.find(1, :include => :firm)
|
||||
end
|
||||
end
|
||||
|
|
|
@ -210,13 +210,6 @@ unless current_adapter?(:SQLServerAdapter, :SybaseAdapter, :OpenBaseAdapter)
|
|||
def setup
|
||||
# Avoid introspection queries during tests.
|
||||
Person.columns; Reader.columns
|
||||
|
||||
@allow_concurrency = ActiveRecord::Base.allow_concurrency
|
||||
ActiveRecord::Base.allow_concurrency = true
|
||||
end
|
||||
|
||||
def teardown
|
||||
ActiveRecord::Base.allow_concurrency = @allow_concurrency
|
||||
end
|
||||
|
||||
# Test typical find.
|
||||
|
@ -264,6 +257,8 @@ unless current_adapter?(:SQLServerAdapter, :SybaseAdapter, :OpenBaseAdapter)
|
|||
end
|
||||
|
||||
if current_adapter?(:PostgreSQLAdapter, :OracleAdapter)
|
||||
use_concurrent_connections
|
||||
|
||||
def test_no_locks_no_wait
|
||||
first, second = duel { Person.find 1 }
|
||||
assert first.end > second.end
|
||||
|
|
|
@ -1,4 +1,5 @@
|
|||
require "cases/helper"
|
||||
require 'models/author'
|
||||
require 'models/developer'
|
||||
require 'models/project'
|
||||
require 'models/comment'
|
||||
|
@ -6,7 +7,7 @@ require 'models/post'
|
|||
require 'models/category'
|
||||
|
||||
class MethodScopingTest < ActiveRecord::TestCase
|
||||
fixtures :developers, :projects, :comments, :posts, :developers_projects
|
||||
fixtures :authors, :developers, :projects, :comments, :posts, :developers_projects
|
||||
|
||||
def test_set_conditions
|
||||
Developer.with_scope(:find => { :conditions => 'just a test...' }) do
|
||||
|
@ -97,6 +98,76 @@ class MethodScopingTest < ActiveRecord::TestCase
|
|||
assert_equal developers(:david).attributes, scoped_developers.first.attributes
|
||||
end
|
||||
|
||||
def test_scoped_find_using_new_style_joins
|
||||
scoped_developers = Developer.with_scope(:find => { :joins => :projects }) do
|
||||
Developer.find(:all, :conditions => 'projects.id = 2')
|
||||
end
|
||||
assert scoped_developers.include?(developers(:david))
|
||||
assert !scoped_developers.include?(developers(:jamis))
|
||||
assert_equal 1, scoped_developers.size
|
||||
assert_equal developers(:david).attributes, scoped_developers.first.attributes
|
||||
end
|
||||
|
||||
def test_scoped_find_merges_old_style_joins
|
||||
scoped_authors = Author.with_scope(:find => { :joins => 'INNER JOIN posts ON authors.id = posts.author_id ' }) do
|
||||
Author.find(:all, :select => 'DISTINCT authors.*', :joins => 'INNER JOIN comments ON posts.id = comments.post_id', :conditions => 'comments.id = 1')
|
||||
end
|
||||
assert scoped_authors.include?(authors(:david))
|
||||
assert !scoped_authors.include?(authors(:mary))
|
||||
assert_equal 1, scoped_authors.size
|
||||
assert_equal authors(:david).attributes, scoped_authors.first.attributes
|
||||
end
|
||||
|
||||
def test_scoped_find_merges_new_style_joins
|
||||
scoped_authors = Author.with_scope(:find => { :joins => :posts }) do
|
||||
Author.find(:all, :select => 'DISTINCT authors.*', :joins => :comments, :conditions => 'comments.id = 1')
|
||||
end
|
||||
assert scoped_authors.include?(authors(:david))
|
||||
assert !scoped_authors.include?(authors(:mary))
|
||||
assert_equal 1, scoped_authors.size
|
||||
assert_equal authors(:david).attributes, scoped_authors.first.attributes
|
||||
end
|
||||
|
||||
def test_scoped_find_merges_new_and_old_style_joins
|
||||
scoped_authors = Author.with_scope(:find => { :joins => :posts }) do
|
||||
Author.find(:all, :select => 'DISTINCT authors.*', :joins => 'JOIN comments ON posts.id = comments.post_id', :conditions => 'comments.id = 1')
|
||||
end
|
||||
assert scoped_authors.include?(authors(:david))
|
||||
assert !scoped_authors.include?(authors(:mary))
|
||||
assert_equal 1, scoped_authors.size
|
||||
assert_equal authors(:david).attributes, scoped_authors.first.attributes
|
||||
end
|
||||
|
||||
def test_scoped_find_merges_string_array_style_and_string_style_joins
|
||||
scoped_authors = Author.with_scope(:find => { :joins => ["INNER JOIN posts ON posts.author_id = authors.id"]}) do
|
||||
Author.find(:all, :select => 'DISTINCT authors.*', :joins => 'INNER JOIN comments ON posts.id = comments.post_id', :conditions => 'comments.id = 1')
|
||||
end
|
||||
assert scoped_authors.include?(authors(:david))
|
||||
assert !scoped_authors.include?(authors(:mary))
|
||||
assert_equal 1, scoped_authors.size
|
||||
assert_equal authors(:david).attributes, scoped_authors.first.attributes
|
||||
end
|
||||
|
||||
def test_scoped_find_merges_string_array_style_and_hash_style_joins
|
||||
scoped_authors = Author.with_scope(:find => { :joins => :posts}) do
|
||||
Author.find(:all, :select => 'DISTINCT authors.*', :joins => ['INNER JOIN comments ON posts.id = comments.post_id'], :conditions => 'comments.id = 1')
|
||||
end
|
||||
assert scoped_authors.include?(authors(:david))
|
||||
assert !scoped_authors.include?(authors(:mary))
|
||||
assert_equal 1, scoped_authors.size
|
||||
assert_equal authors(:david).attributes, scoped_authors.first.attributes
|
||||
end
|
||||
|
||||
def test_scoped_find_merges_joins_and_eliminates_duplicate_string_joins
|
||||
scoped_authors = Author.with_scope(:find => { :joins => 'INNER JOIN posts ON posts.author_id = authors.id'}) do
|
||||
Author.find(:all, :select => 'DISTINCT authors.*', :joins => ["INNER JOIN posts ON posts.author_id = authors.id", "INNER JOIN comments ON posts.id = comments.post_id"], :conditions => 'comments.id = 1')
|
||||
end
|
||||
assert scoped_authors.include?(authors(:david))
|
||||
assert !scoped_authors.include?(authors(:mary))
|
||||
assert_equal 1, scoped_authors.size
|
||||
assert_equal authors(:david).attributes, scoped_authors.first.attributes
|
||||
end
|
||||
|
||||
def test_scoped_count_include
|
||||
# with the include, will retrieve only developers for the given project
|
||||
Developer.with_scope(:find => { :include => :projects }) do
|
||||
|
@ -152,7 +223,7 @@ class MethodScopingTest < ActiveRecord::TestCase
|
|||
end
|
||||
|
||||
class NestedScopingTest < ActiveRecord::TestCase
|
||||
fixtures :developers, :projects, :comments, :posts
|
||||
fixtures :authors, :developers, :projects, :comments, :posts
|
||||
|
||||
def test_merge_options
|
||||
Developer.with_scope(:find => { :conditions => 'salary = 80000' }) do
|
||||
|
@ -357,6 +428,42 @@ class NestedScopingTest < ActiveRecord::TestCase
|
|||
assert_equal scoped_methods, Developer.instance_eval('current_scoped_methods')
|
||||
end
|
||||
end
|
||||
|
||||
def test_nested_scoped_find_merges_old_style_joins
|
||||
scoped_authors = Author.with_scope(:find => { :joins => 'INNER JOIN posts ON authors.id = posts.author_id' }) do
|
||||
Author.with_scope(:find => { :joins => 'INNER JOIN comments ON posts.id = comments.post_id' }) do
|
||||
Author.find(:all, :select => 'DISTINCT authors.*', :conditions => 'comments.id = 1')
|
||||
end
|
||||
end
|
||||
assert scoped_authors.include?(authors(:david))
|
||||
assert !scoped_authors.include?(authors(:mary))
|
||||
assert_equal 1, scoped_authors.size
|
||||
assert_equal authors(:david).attributes, scoped_authors.first.attributes
|
||||
end
|
||||
|
||||
def test_nested_scoped_find_merges_new_style_joins
|
||||
scoped_authors = Author.with_scope(:find => { :joins => :posts }) do
|
||||
Author.with_scope(:find => { :joins => :comments }) do
|
||||
Author.find(:all, :select => 'DISTINCT authors.*', :conditions => 'comments.id = 1')
|
||||
end
|
||||
end
|
||||
assert scoped_authors.include?(authors(:david))
|
||||
assert !scoped_authors.include?(authors(:mary))
|
||||
assert_equal 1, scoped_authors.size
|
||||
assert_equal authors(:david).attributes, scoped_authors.first.attributes
|
||||
end
|
||||
|
||||
def test_nested_scoped_find_merges_new_and_old_style_joins
|
||||
scoped_authors = Author.with_scope(:find => { :joins => :posts }) do
|
||||
Author.with_scope(:find => { :joins => 'INNER JOIN comments ON posts.id = comments.post_id' }) do
|
||||
Author.find(:all, :select => 'DISTINCT authors.*', :joins => '', :conditions => 'comments.id = 1')
|
||||
end
|
||||
end
|
||||
assert scoped_authors.include?(authors(:david))
|
||||
assert !scoped_authors.include?(authors(:mary))
|
||||
assert_equal 1, scoped_authors.size
|
||||
assert_equal authors(:david).attributes, scoped_authors.first.attributes
|
||||
end
|
||||
end
|
||||
|
||||
class HasManyScopingTest< ActiveRecord::TestCase
|
||||
|
|
|
@ -934,6 +934,21 @@ if ActiveRecord::Base.connection.supports_migrations?
|
|||
assert_equal(0, ActiveRecord::Migrator.current_version)
|
||||
end
|
||||
|
||||
if current_adapter?(:PostgreSQLAdapter)
|
||||
def test_migrator_one_up_with_exception_and_rollback
|
||||
assert !Person.column_methods_hash.include?(:last_name)
|
||||
|
||||
e = assert_raises(StandardError) do
|
||||
ActiveRecord::Migrator.up(MIGRATIONS_ROOT + "/broken", 100)
|
||||
end
|
||||
|
||||
assert_equal "An error has occurred, this and all later migrations canceled:\n\nSomething broke", e.message
|
||||
|
||||
Person.reset_column_information
|
||||
assert !Person.column_methods_hash.include?(:last_name)
|
||||
end
|
||||
end
|
||||
|
||||
def test_finds_migrations
|
||||
migrations = ActiveRecord::Migrator.new(:up, MIGRATIONS_ROOT + "/valid").migrations
|
||||
[['1', 'people_have_last_names'],
|
||||
|
@ -952,6 +967,26 @@ if ActiveRecord::Base.connection.supports_migrations?
|
|||
migrations[0].name == 'innocent_jointable'
|
||||
end
|
||||
|
||||
def test_only_loads_pending_migrations
|
||||
# migrate up to 1
|
||||
ActiveRecord::Migrator.up(MIGRATIONS_ROOT + "/valid", 1)
|
||||
|
||||
# now unload the migrations that have been defined
|
||||
PeopleHaveLastNames.unloadable
|
||||
ActiveSupport::Dependencies.remove_unloadable_constants!
|
||||
|
||||
ActiveRecord::Migrator.migrate(MIGRATIONS_ROOT + "/valid", nil)
|
||||
|
||||
assert !defined? PeopleHaveLastNames
|
||||
|
||||
%w(WeNeedReminders, InnocentJointable).each do |migration|
|
||||
assert defined? migration
|
||||
end
|
||||
|
||||
ensure
|
||||
load(MIGRATIONS_ROOT + "/valid/1_people_have_last_names.rb")
|
||||
end
|
||||
|
||||
def test_migrator_interleaved_migrations
|
||||
ActiveRecord::Migrator.up(MIGRATIONS_ROOT + "/interleaved/pass_1")
|
||||
|
||||
|
@ -1098,7 +1133,11 @@ if ActiveRecord::Base.connection.supports_migrations?
|
|||
columns = Person.connection.columns(:binary_testings)
|
||||
data_column = columns.detect { |c| c.name == "data" }
|
||||
|
||||
assert_nil data_column.default
|
||||
if current_adapter?(:MysqlAdapter)
|
||||
assert_equal '', data_column.default
|
||||
else
|
||||
assert_nil data_column.default
|
||||
end
|
||||
|
||||
Person.connection.drop_table :binary_testings rescue nil
|
||||
end
|
||||
|
|
|
@ -57,4 +57,29 @@ class MultipleDbTest < ActiveRecord::TestCase
|
|||
|
||||
assert Course.connection
|
||||
end
|
||||
|
||||
def test_transactions_across_databases
|
||||
c1 = Course.find(1)
|
||||
e1 = Entrant.find(1)
|
||||
|
||||
begin
|
||||
Course.transaction do
|
||||
Entrant.transaction do
|
||||
c1.name = "Typo"
|
||||
e1.name = "Typo"
|
||||
c1.save
|
||||
e1.save
|
||||
raise "No I messed up."
|
||||
end
|
||||
end
|
||||
rescue
|
||||
# Yup caught it
|
||||
end
|
||||
|
||||
assert_equal "Typo", c1.name
|
||||
assert_equal "Typo", e1.name
|
||||
|
||||
assert_equal "Ruby Development", Course.find(1).name
|
||||
assert_equal "Ruby Developer", Entrant.find(1).name
|
||||
end
|
||||
end
|
||||
|
|
|
@ -77,6 +77,10 @@ class NamedScopeTest < ActiveRecord::TestCase
|
|||
assert_equal Topic.replied.approved, Topic.replied.approved_as_string
|
||||
end
|
||||
|
||||
def test_scopes_can_be_specified_with_deep_hash_conditions
|
||||
assert_equal Topic.replied.approved, Topic.replied.approved_as_hash_condition
|
||||
end
|
||||
|
||||
def test_scopes_are_composable
|
||||
assert_equal (approved = Topic.find(:all, :conditions => {:approved => true})), Topic.approved
|
||||
assert_equal (replied = Topic.find(:all, :conditions => 'replies_count > 0')), Topic.replied
|
||||
|
@ -192,6 +196,55 @@ class NamedScopeTest < ActiveRecord::TestCase
|
|||
end
|
||||
end
|
||||
|
||||
def test_any_should_not_load_results
|
||||
topics = Topic.base
|
||||
assert_queries(2) do
|
||||
topics.any? # use count query
|
||||
topics.collect # force load
|
||||
topics.any? # use loaded (no query)
|
||||
end
|
||||
end
|
||||
|
||||
def test_any_should_call_proxy_found_if_using_a_block
|
||||
topics = Topic.base
|
||||
assert_queries(1) do
|
||||
topics.expects(:empty?).never
|
||||
topics.any? { true }
|
||||
end
|
||||
end
|
||||
|
||||
def test_any_should_not_fire_query_if_named_scope_loaded
|
||||
topics = Topic.base
|
||||
topics.collect # force load
|
||||
assert_no_queries { assert topics.any? }
|
||||
end
|
||||
|
||||
def test_should_build_with_proxy_options
|
||||
topic = Topic.approved.build({})
|
||||
assert topic.approved
|
||||
end
|
||||
|
||||
def test_should_build_new_with_proxy_options
|
||||
topic = Topic.approved.new
|
||||
assert topic.approved
|
||||
end
|
||||
|
||||
def test_should_create_with_proxy_options
|
||||
topic = Topic.approved.create({})
|
||||
assert topic.approved
|
||||
end
|
||||
|
||||
def test_should_create_with_bang_with_proxy_options
|
||||
topic = Topic.approved.create!({})
|
||||
assert topic.approved
|
||||
end
|
||||
|
||||
def test_should_build_with_proxy_options_chained
|
||||
topic = Topic.approved.by_lifo.build({})
|
||||
assert topic.approved
|
||||
assert_equal 'lifo', topic.author_name
|
||||
end
|
||||
|
||||
def test_find_all_should_behave_like_select
|
||||
assert_equal Topic.base.select(&:approved), Topic.base.find_all(&:approved)
|
||||
end
|
||||
|
@ -203,4 +256,25 @@ class NamedScopeTest < ActiveRecord::TestCase
|
|||
def test_should_use_where_in_query_for_named_scope
|
||||
assert_equal Developer.find_all_by_name('Jamis'), Developer.find_all_by_id(Developer.jamises)
|
||||
end
|
||||
|
||||
def test_size_should_use_count_when_results_are_not_loaded
|
||||
topics = Topic.base
|
||||
assert_queries(1) do
|
||||
assert_sql(/COUNT/i) { topics.size }
|
||||
end
|
||||
end
|
||||
|
||||
def test_size_should_use_length_when_results_are_loaded
|
||||
topics = Topic.base
|
||||
topics.reload # force load
|
||||
assert_no_queries do
|
||||
topics.size # use loaded (no query)
|
||||
end
|
||||
end
|
||||
|
||||
def test_chaining_with_duplicate_joins
|
||||
join = "INNER JOIN comments ON comments.post_id = posts.id"
|
||||
post = Post.find(1)
|
||||
assert_equal post.comments.size, Post.scoped(:joins => join).scoped(:joins => join, :conditions => "posts.id = #{post.id}").size
|
||||
end
|
||||
end
|
||||
|
|
87
vendor/rails/activerecord/test/cases/pooled_connections_test.rb
vendored
Normal file
87
vendor/rails/activerecord/test/cases/pooled_connections_test.rb
vendored
Normal file
|
@ -0,0 +1,87 @@
|
|||
require "cases/helper"
|
||||
|
||||
class PooledConnectionsTest < ActiveRecord::TestCase
|
||||
def setup
|
||||
super
|
||||
@connection = ActiveRecord::Base.remove_connection
|
||||
end
|
||||
|
||||
def teardown
|
||||
ActiveRecord::Base.clear_all_connections!
|
||||
ActiveRecord::Base.establish_connection(@connection)
|
||||
super
|
||||
end
|
||||
|
||||
def checkout_connections
|
||||
ActiveRecord::Base.establish_connection(@connection.merge({:pool => 2, :wait_timeout => 0.3}))
|
||||
@connections = []
|
||||
@timed_out = 0
|
||||
|
||||
4.times do
|
||||
Thread.new do
|
||||
begin
|
||||
@connections << ActiveRecord::Base.connection_pool.checkout
|
||||
rescue ActiveRecord::ConnectionTimeoutError
|
||||
@timed_out += 1
|
||||
end
|
||||
end.join
|
||||
end
|
||||
end
|
||||
|
||||
def test_pooled_connection_checkout
|
||||
checkout_connections
|
||||
assert_equal @connections.length, 2
|
||||
assert_equal @timed_out, 2
|
||||
end
|
||||
|
||||
def checkout_checkin_connections(pool_size, threads)
|
||||
ActiveRecord::Base.establish_connection(@connection.merge({:pool => pool_size, :wait_timeout => 0.5}))
|
||||
@connection_count = 0
|
||||
@timed_out = 0
|
||||
threads.times do
|
||||
Thread.new do
|
||||
begin
|
||||
conn = ActiveRecord::Base.connection_pool.checkout
|
||||
sleep 0.1
|
||||
ActiveRecord::Base.connection_pool.checkin conn
|
||||
@connection_count += 1
|
||||
rescue ActiveRecord::ConnectionTimeoutError
|
||||
@timed_out += 1
|
||||
end
|
||||
end.join
|
||||
end
|
||||
end
|
||||
|
||||
def test_pooled_connection_checkin_one
|
||||
checkout_checkin_connections 1, 2
|
||||
assert_equal 2, @connection_count
|
||||
assert_equal 0, @timed_out
|
||||
end
|
||||
|
||||
def test_pooled_connection_checkin_two
|
||||
checkout_checkin_connections 2, 3
|
||||
assert_equal 3, @connection_count
|
||||
assert_equal 0, @timed_out
|
||||
end
|
||||
|
||||
def test_pooled_connection_checkout_existing_first
|
||||
ActiveRecord::Base.establish_connection(@connection.merge({:pool => 1}))
|
||||
conn_pool = ActiveRecord::Base.connection_pool
|
||||
conn = conn_pool.checkout
|
||||
conn_pool.checkin(conn)
|
||||
conn = conn_pool.checkout
|
||||
assert ActiveRecord::ConnectionAdapters::AbstractAdapter === conn
|
||||
conn_pool.checkin(conn)
|
||||
end
|
||||
end unless %w(FrontBase).include? ActiveRecord::Base.connection.adapter_name
|
||||
|
||||
class AllowConcurrencyDeprecatedTest < ActiveRecord::TestCase
|
||||
def test_allow_concurrency_is_deprecated
|
||||
assert_deprecated('ActiveRecord::Base.allow_concurrency') do
|
||||
ActiveRecord::Base.allow_concurrency
|
||||
end
|
||||
assert_deprecated('ActiveRecord::Base.allow_concurrency=') do
|
||||
ActiveRecord::Base.allow_concurrency = true
|
||||
end
|
||||
end
|
||||
end
|
|
@ -160,12 +160,20 @@ class ReflectionTest < ActiveRecord::TestCase
|
|||
|
||||
def test_reflection_of_all_associations
|
||||
# FIXME these assertions bust a lot
|
||||
assert_equal 22, Firm.reflect_on_all_associations.size
|
||||
assert_equal 17, Firm.reflect_on_all_associations(:has_many).size
|
||||
assert_equal 5, Firm.reflect_on_all_associations(:has_one).size
|
||||
assert_equal 26, Firm.reflect_on_all_associations.size
|
||||
assert_equal 20, Firm.reflect_on_all_associations(:has_many).size
|
||||
assert_equal 6, Firm.reflect_on_all_associations(:has_one).size
|
||||
assert_equal 0, Firm.reflect_on_all_associations(:belongs_to).size
|
||||
end
|
||||
|
||||
def test_reflection_should_not_raise_error_when_compared_to_other_object
|
||||
assert_nothing_raised { Firm.reflections[:clients] == Object.new }
|
||||
end
|
||||
|
||||
def test_has_many_through_reflection
|
||||
assert_kind_of ActiveRecord::Reflection::ThroughReflection, Subscriber.reflect_on_association(:books)
|
||||
end
|
||||
|
||||
private
|
||||
def assert_reflection(klass, association, options)
|
||||
assert reflection = klass.reflect_on_association(association)
|
||||
|
|
20
vendor/rails/activerecord/test/cases/reload_models_test.rb
vendored
Normal file
20
vendor/rails/activerecord/test/cases/reload_models_test.rb
vendored
Normal file
|
@ -0,0 +1,20 @@
|
|||
require "cases/helper"
|
||||
require 'models/owner'
|
||||
require 'models/pet'
|
||||
|
||||
class ReloadModelsTest < ActiveRecord::TestCase
|
||||
def test_has_one_with_reload
|
||||
pet = Pet.find_by_name('parrot')
|
||||
pet.owner = Owner.find_by_name('ashley')
|
||||
|
||||
# Reload the class Owner, simulating auto-reloading of model classes in a
|
||||
# development environment. Note that meanwhile the class Pet is not
|
||||
# reloaded, simulating a class that is present in a plugin.
|
||||
Object.class_eval { remove_const :Owner }
|
||||
Kernel.load(File.expand_path(File.join(File.dirname(__FILE__), "../models/owner.rb")))
|
||||
|
||||
pet = Pet.find_by_name('parrot')
|
||||
pet.owner = Owner.find_by_name('ashley')
|
||||
assert_equal pet.owner, Owner.find_by_name('ashley')
|
||||
end
|
||||
end
|
25
vendor/rails/activerecord/test/cases/sanitize_test.rb
vendored
Normal file
25
vendor/rails/activerecord/test/cases/sanitize_test.rb
vendored
Normal file
|
@ -0,0 +1,25 @@
|
|||
require "cases/helper"
|
||||
require 'models/binary'
|
||||
|
||||
class SanitizeTest < ActiveRecord::TestCase
|
||||
def setup
|
||||
end
|
||||
|
||||
def test_sanitize_sql_array_handles_string_interpolation
|
||||
quoted_bambi = ActiveRecord::Base.connection.quote_string("Bambi")
|
||||
assert_equal "name=#{quoted_bambi}", Binary.send(:sanitize_sql_array, ["name=%s", "Bambi"])
|
||||
assert_equal "name=#{quoted_bambi}", Binary.send(:sanitize_sql_array, ["name=%s", "Bambi".mb_chars])
|
||||
quoted_bambi_and_thumper = ActiveRecord::Base.connection.quote_string("Bambi\nand\nThumper")
|
||||
assert_equal "name=#{quoted_bambi_and_thumper}",Binary.send(:sanitize_sql_array, ["name=%s", "Bambi\nand\nThumper"])
|
||||
assert_equal "name=#{quoted_bambi_and_thumper}",Binary.send(:sanitize_sql_array, ["name=%s", "Bambi\nand\nThumper".mb_chars])
|
||||
end
|
||||
|
||||
def test_sanitize_sql_array_handles_bind_variables
|
||||
quoted_bambi = ActiveRecord::Base.connection.quote("Bambi")
|
||||
assert_equal "name=#{quoted_bambi}", Binary.send(:sanitize_sql_array, ["name=?", "Bambi"])
|
||||
assert_equal "name=#{quoted_bambi}", Binary.send(:sanitize_sql_array, ["name=?", "Bambi".mb_chars])
|
||||
quoted_bambi_and_thumper = ActiveRecord::Base.connection.quote("Bambi\nand\nThumper")
|
||||
assert_equal "name=#{quoted_bambi_and_thumper}", Binary.send(:sanitize_sql_array, ["name=?", "Bambi\nand\nThumper"])
|
||||
assert_equal "name=#{quoted_bambi_and_thumper}", Binary.send(:sanitize_sql_array, ["name=?", "Bambi\nand\nThumper".mb_chars])
|
||||
end
|
||||
end
|
|
@ -18,8 +18,8 @@ class SchemaAuthorizationTest < ActiveRecord::TestCase
|
|||
@connection.execute "SET search_path TO '$user',public"
|
||||
set_session_auth
|
||||
USERS.each do |u|
|
||||
@connection.execute "CREATE USER #{u}"
|
||||
@connection.execute "CREATE SCHEMA AUTHORIZATION #{u}"
|
||||
@connection.execute "CREATE USER #{u}" rescue nil
|
||||
@connection.execute "CREATE SCHEMA AUTHORIZATION #{u}" rescue nil
|
||||
set_session_auth u
|
||||
@connection.execute "CREATE TABLE #{TABLE_NAME} (#{COLUMNS.join(',')})"
|
||||
@connection.execute "INSERT INTO #{TABLE_NAME} (name) VALUES ('#{u}')"
|
||||
|
|
|
@ -2,6 +2,7 @@ require "cases/helper"
|
|||
require 'models/topic'
|
||||
require 'models/reply'
|
||||
require 'models/developer'
|
||||
require 'models/book'
|
||||
|
||||
class TransactionTest < ActiveRecord::TestCase
|
||||
self.use_transactional_fixtures = false
|
||||
|
@ -86,8 +87,7 @@ class TransactionTest < ActiveRecord::TestCase
|
|||
assert Topic.find(2).approved?, "Second should still be approved"
|
||||
end
|
||||
|
||||
|
||||
def test_callback_rollback_in_save
|
||||
def test_raising_exception_in_callback_rollbacks_in_save
|
||||
add_exception_raising_after_save_callback_to_topic
|
||||
|
||||
begin
|
||||
|
@ -102,6 +102,54 @@ class TransactionTest < ActiveRecord::TestCase
|
|||
end
|
||||
end
|
||||
|
||||
def test_cancellation_from_before_destroy_rollbacks_in_destroy
|
||||
add_cancelling_before_destroy_with_db_side_effect_to_topic
|
||||
begin
|
||||
nbooks_before_destroy = Book.count
|
||||
status = @first.destroy
|
||||
assert !status
|
||||
assert_nothing_raised(ActiveRecord::RecordNotFound) { @first.reload }
|
||||
assert_equal nbooks_before_destroy, Book.count
|
||||
ensure
|
||||
remove_cancelling_before_destroy_with_db_side_effect_to_topic
|
||||
end
|
||||
end
|
||||
|
||||
def test_cancellation_from_before_filters_rollbacks_in_save
|
||||
%w(validation save).each do |filter|
|
||||
send("add_cancelling_before_#{filter}_with_db_side_effect_to_topic")
|
||||
begin
|
||||
nbooks_before_save = Book.count
|
||||
original_author_name = @first.author_name
|
||||
@first.author_name += '_this_should_not_end_up_in_the_db'
|
||||
status = @first.save
|
||||
assert !status
|
||||
assert_equal original_author_name, @first.reload.author_name
|
||||
assert_equal nbooks_before_save, Book.count
|
||||
ensure
|
||||
send("remove_cancelling_before_#{filter}_with_db_side_effect_to_topic")
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
def test_cancellation_from_before_filters_rollbacks_in_save!
|
||||
%w(validation save).each do |filter|
|
||||
send("add_cancelling_before_#{filter}_with_db_side_effect_to_topic")
|
||||
begin
|
||||
nbooks_before_save = Book.count
|
||||
original_author_name = @first.author_name
|
||||
@first.author_name += '_this_should_not_end_up_in_the_db'
|
||||
@first.save!
|
||||
flunk
|
||||
rescue => e
|
||||
assert_equal original_author_name, @first.reload.author_name
|
||||
assert_equal nbooks_before_save, Book.count
|
||||
ensure
|
||||
send("remove_cancelling_before_#{filter}_with_db_side_effect_to_topic")
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
def test_callback_rollback_in_create
|
||||
new_topic = Topic.new(
|
||||
:title => "A new topic",
|
||||
|
@ -168,6 +216,7 @@ class TransactionTest < ActiveRecord::TestCase
|
|||
uses_mocha 'mocking connection.commit_db_transaction' do
|
||||
def test_rollback_when_commit_raises
|
||||
Topic.connection.expects(:begin_db_transaction)
|
||||
Topic.connection.expects(:transaction_active?).returns(true) if current_adapter?(:PostgreSQLAdapter)
|
||||
Topic.connection.expects(:commit_db_transaction).raises('OH NOES')
|
||||
Topic.connection.expects(:rollback_db_transaction)
|
||||
|
||||
|
@ -221,20 +270,21 @@ class TransactionTest < ActiveRecord::TestCase
|
|||
def remove_exception_raising_after_create_callback_to_topic
|
||||
Topic.class_eval { remove_method :after_create }
|
||||
end
|
||||
|
||||
%w(validation save destroy).each do |filter|
|
||||
define_method("add_cancelling_before_#{filter}_with_db_side_effect_to_topic") do
|
||||
Topic.class_eval "def before_#{filter}() Book.create; false end"
|
||||
end
|
||||
|
||||
define_method("remove_cancelling_before_#{filter}_with_db_side_effect_to_topic") do
|
||||
Topic.class_eval "remove_method :before_#{filter}"
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
if current_adapter?(:PostgreSQLAdapter)
|
||||
class ConcurrentTransactionTest < TransactionTest
|
||||
def setup
|
||||
@allow_concurrency = ActiveRecord::Base.allow_concurrency
|
||||
ActiveRecord::Base.allow_concurrency = true
|
||||
super
|
||||
end
|
||||
|
||||
def teardown
|
||||
super
|
||||
ActiveRecord::Base.allow_concurrency = @allow_concurrency
|
||||
end
|
||||
use_concurrent_connections
|
||||
|
||||
# This will cause transactions to overlap and fail unless they are performed on
|
||||
# separate database connections.
|
||||
|
|
921
vendor/rails/activerecord/test/cases/validations_i18n_test.rb
vendored
Normal file
921
vendor/rails/activerecord/test/cases/validations_i18n_test.rb
vendored
Normal file
|
@ -0,0 +1,921 @@
|
|||
require "cases/helper"
|
||||
require 'models/topic'
|
||||
require 'models/reply'
|
||||
|
||||
class ActiveRecordValidationsI18nTests < Test::Unit::TestCase
|
||||
def setup
|
||||
reset_callbacks Topic
|
||||
@topic = Topic.new
|
||||
@old_load_path, @old_backend = I18n.load_path, I18n.backend
|
||||
I18n.load_path.clear
|
||||
I18n.backend = I18n::Backend::Simple.new
|
||||
I18n.backend.store_translations('en-US', :activerecord => {:errors => {:messages => {:custom => nil}}})
|
||||
end
|
||||
|
||||
def teardown
|
||||
reset_callbacks Topic
|
||||
I18n.load_path.replace @old_load_path
|
||||
I18n.backend = @old_backend
|
||||
end
|
||||
|
||||
def unique_topic
|
||||
@unique ||= Topic.create :title => 'unique!'
|
||||
end
|
||||
|
||||
def replied_topic
|
||||
@replied_topic ||= begin
|
||||
topic = Topic.create(:title => "topic")
|
||||
topic.replies << Reply.new
|
||||
topic
|
||||
end
|
||||
end
|
||||
|
||||
def reset_callbacks(*models)
|
||||
models.each do |model|
|
||||
model.instance_variable_set("@validate_callbacks", ActiveSupport::Callbacks::CallbackChain.new)
|
||||
model.instance_variable_set("@validate_on_create_callbacks", ActiveSupport::Callbacks::CallbackChain.new)
|
||||
model.instance_variable_set("@validate_on_update_callbacks", ActiveSupport::Callbacks::CallbackChain.new)
|
||||
end
|
||||
end
|
||||
|
||||
def test_default_error_messages_is_deprecated
|
||||
assert_deprecated('ActiveRecord::Errors.default_error_messages') do
|
||||
ActiveRecord::Errors.default_error_messages
|
||||
end
|
||||
end
|
||||
|
||||
def test_percent_s_interpolation_syntax_in_error_messages_still_works
|
||||
ActiveSupport::Deprecation.silence do
|
||||
result = I18n.t :does_not_exist, :default => "%s interpolation syntax is deprecated", :value => 'this'
|
||||
assert_equal result, "this interpolation syntax is deprecated"
|
||||
end
|
||||
end
|
||||
|
||||
def test_percent_s_interpolation_syntax_in_error_messages_is_deprecated
|
||||
assert_deprecated('using %s in messages') do
|
||||
I18n.t :does_not_exist, :default => "%s interpolation syntax is deprected", :value => 'this'
|
||||
end
|
||||
end
|
||||
|
||||
def test_percent_d_interpolation_syntax_in_error_messages_still_works
|
||||
ActiveSupport::Deprecation.silence do
|
||||
result = I18n.t :does_not_exist, :default => "%d interpolation syntaxes are deprecated", :count => 2
|
||||
assert_equal result, "2 interpolation syntaxes are deprecated"
|
||||
end
|
||||
end
|
||||
|
||||
def test_percent_d_interpolation_syntax_in_error_messages_is_deprecated
|
||||
assert_deprecated('using %d in messages') do
|
||||
I18n.t :does_not_exist, :default => "%d interpolation syntaxes are deprected", :count => 2
|
||||
end
|
||||
end
|
||||
|
||||
# ActiveRecord::Errors
|
||||
uses_mocha 'ActiveRecord::Errors' do
|
||||
|
||||
def test_errors_generate_message_translates_custom_model_attribute_key
|
||||
|
||||
I18n.expects(:translate).with(
|
||||
:topic,
|
||||
{ :count => 1,
|
||||
:default => ['Topic'],
|
||||
:scope => [:activerecord, :models]
|
||||
}
|
||||
).returns('Topic')
|
||||
|
||||
I18n.expects(:translate).with(
|
||||
:"topic.title",
|
||||
{ :count => 1,
|
||||
:default => ['Title'],
|
||||
:scope => [:activerecord, :attributes]
|
||||
}
|
||||
).returns('Title')
|
||||
|
||||
I18n.expects(:translate).with(
|
||||
:"models.topic.attributes.title.invalid",
|
||||
:value => nil,
|
||||
:scope => [:activerecord, :errors],
|
||||
:default => [
|
||||
:"models.topic.invalid",
|
||||
'default from class def error 1',
|
||||
:"messages.invalid"],
|
||||
:attribute => "Title",
|
||||
:model => "Topic"
|
||||
).returns('default from class def error 1')
|
||||
|
||||
@topic.errors.generate_message :title, :invalid, :default => 'default from class def error 1'
|
||||
end
|
||||
|
||||
def test_errors_generate_message_translates_custom_model_attribute_keys_with_sti
|
||||
|
||||
I18n.expects(:translate).with(
|
||||
:reply,
|
||||
{ :count => 1,
|
||||
:default => [:topic, 'Reply'],
|
||||
:scope => [:activerecord, :models]
|
||||
}
|
||||
).returns('Reply')
|
||||
|
||||
I18n.expects(:translate).with(
|
||||
:"reply.title",
|
||||
{ :count => 1,
|
||||
:default => [:'topic.title', 'Title'],
|
||||
:scope => [:activerecord, :attributes]
|
||||
}
|
||||
).returns('Title')
|
||||
|
||||
I18n.expects(:translate).with(
|
||||
:"models.reply.attributes.title.invalid",
|
||||
:value => nil,
|
||||
:scope => [:activerecord, :errors],
|
||||
:default => [
|
||||
:"models.reply.invalid",
|
||||
:"models.topic.attributes.title.invalid",
|
||||
:"models.topic.invalid",
|
||||
'default from class def',
|
||||
:"messages.invalid"],
|
||||
:model => 'Reply',
|
||||
:attribute => 'Title'
|
||||
).returns("default from class def")
|
||||
|
||||
Reply.new.errors.generate_message :title, :invalid, :default => 'default from class def'
|
||||
|
||||
end
|
||||
|
||||
def test_errors_add_on_empty_generates_message
|
||||
@topic.errors.expects(:generate_message).with(:title, :empty, {:default => nil})
|
||||
@topic.errors.add_on_empty :title
|
||||
end
|
||||
|
||||
def test_errors_add_on_empty_generates_message_with_custom_default_message
|
||||
@topic.errors.expects(:generate_message).with(:title, :empty, {:default => 'custom'})
|
||||
@topic.errors.add_on_empty :title, 'custom'
|
||||
end
|
||||
|
||||
def test_errors_add_on_blank_generates_message
|
||||
@topic.errors.expects(:generate_message).with(:title, :blank, {:default => nil})
|
||||
@topic.errors.add_on_blank :title
|
||||
end
|
||||
|
||||
def test_errors_add_on_blank_generates_message_with_custom_default_message
|
||||
@topic.errors.expects(:generate_message).with(:title, :blank, {:default => 'custom'})
|
||||
@topic.errors.add_on_blank :title, 'custom'
|
||||
end
|
||||
|
||||
def test_errors_full_messages_translates_human_attribute_name_for_model_attributes
|
||||
@topic.errors.instance_variable_set :@errors, { 'title' => ['empty'] }
|
||||
I18n.expects(:translate).with(:"topic.title", :default => ['Title'], :scope => [:activerecord, :attributes], :count => 1).returns('Title')
|
||||
@topic.errors.full_messages :locale => 'en-US'
|
||||
end
|
||||
end
|
||||
|
||||
# ActiveRecord::Validations
|
||||
uses_mocha 'ActiveRecord::Validations' do
|
||||
# validates_confirmation_of w/ mocha
|
||||
|
||||
def test_validates_confirmation_of_generates_message
|
||||
Topic.validates_confirmation_of :title
|
||||
@topic.title_confirmation = 'foo'
|
||||
@topic.errors.expects(:generate_message).with(:title, :confirmation, {:default => nil})
|
||||
@topic.valid?
|
||||
end
|
||||
|
||||
def test_validates_confirmation_of_generates_message_with_custom_default_message
|
||||
Topic.validates_confirmation_of :title, :message => 'custom'
|
||||
@topic.title_confirmation = 'foo'
|
||||
@topic.errors.expects(:generate_message).with(:title, :confirmation, {:default => 'custom'})
|
||||
@topic.valid?
|
||||
end
|
||||
|
||||
# validates_acceptance_of w/ mocha
|
||||
|
||||
def test_validates_acceptance_of_generates_message
|
||||
Topic.validates_acceptance_of :title, :allow_nil => false
|
||||
@topic.errors.expects(:generate_message).with(:title, :accepted, {:default => nil})
|
||||
@topic.valid?
|
||||
end
|
||||
|
||||
def test_validates_acceptance_of_generates_message_with_custom_default_message
|
||||
Topic.validates_acceptance_of :title, :message => 'custom', :allow_nil => false
|
||||
@topic.errors.expects(:generate_message).with(:title, :accepted, {:default => 'custom'})
|
||||
@topic.valid?
|
||||
end
|
||||
|
||||
# validates_presence_of w/ mocha
|
||||
|
||||
def test_validates_presence_of_generates_message
|
||||
Topic.validates_presence_of :title
|
||||
@topic.errors.expects(:generate_message).with(:title, :blank, {:default => nil})
|
||||
@topic.valid?
|
||||
end
|
||||
|
||||
def test_validates_presence_of_generates_message_with_custom_default_message
|
||||
Topic.validates_presence_of :title, :message => 'custom'
|
||||
@topic.errors.expects(:generate_message).with(:title, :blank, {:default => 'custom'})
|
||||
@topic.valid?
|
||||
end
|
||||
|
||||
def test_validates_length_of_within_generates_message_with_title_too_short
|
||||
Topic.validates_length_of :title, :within => 3..5
|
||||
@topic.errors.expects(:generate_message).with(:title, :too_short, {:count => 3, :default => nil})
|
||||
@topic.valid?
|
||||
end
|
||||
|
||||
def test_validates_length_of_within_generates_message_with_title_too_short_and_custom_default_message
|
||||
Topic.validates_length_of :title, :within => 3..5, :too_short => 'custom'
|
||||
@topic.errors.expects(:generate_message).with(:title, :too_short, {:count => 3, :default => 'custom'})
|
||||
@topic.valid?
|
||||
end
|
||||
|
||||
def test_validates_length_of_within_generates_message_with_title_too_long
|
||||
Topic.validates_length_of :title, :within => 3..5
|
||||
@topic.title = 'this title is too long'
|
||||
@topic.errors.expects(:generate_message).with(:title, :too_long, {:count => 5, :default => nil})
|
||||
@topic.valid?
|
||||
end
|
||||
|
||||
def test_validates_length_of_within_generates_message_with_title_too_long_and_custom_default_message
|
||||
Topic.validates_length_of :title, :within => 3..5, :too_long => 'custom'
|
||||
@topic.title = 'this title is too long'
|
||||
@topic.errors.expects(:generate_message).with(:title, :too_long, {:count => 5, :default => 'custom'})
|
||||
@topic.valid?
|
||||
end
|
||||
|
||||
# validates_length_of :within w/ mocha
|
||||
|
||||
def test_validates_length_of_within_generates_message_with_title_too_short
|
||||
Topic.validates_length_of :title, :within => 3..5
|
||||
@topic.errors.expects(:generate_message).with(:title, :too_short, {:count => 3, :default => nil})
|
||||
@topic.valid?
|
||||
end
|
||||
|
||||
def test_validates_length_of_within_generates_message_with_title_too_short_and_custom_default_message
|
||||
Topic.validates_length_of :title, :within => 3..5, :too_short => 'custom'
|
||||
@topic.errors.expects(:generate_message).with(:title, :too_short, {:count => 3, :default => 'custom'})
|
||||
@topic.valid?
|
||||
end
|
||||
|
||||
def test_validates_length_of_within_generates_message_with_title_too_long
|
||||
Topic.validates_length_of :title, :within => 3..5
|
||||
@topic.title = 'this title is too long'
|
||||
@topic.errors.expects(:generate_message).with(:title, :too_long, {:count => 5, :default => nil})
|
||||
@topic.valid?
|
||||
end
|
||||
|
||||
def test_validates_length_of_within_generates_message_with_title_too_long_and_custom_default_message
|
||||
Topic.validates_length_of :title, :within => 3..5, :too_long => 'custom'
|
||||
@topic.title = 'this title is too long'
|
||||
@topic.errors.expects(:generate_message).with(:title, :too_long, {:count => 5, :default => 'custom'})
|
||||
@topic.valid?
|
||||
end
|
||||
|
||||
# validates_length_of :is w/ mocha
|
||||
|
||||
def test_validates_length_of_is_generates_message
|
||||
Topic.validates_length_of :title, :is => 5
|
||||
@topic.errors.expects(:generate_message).with(:title, :wrong_length, {:count => 5, :default => nil})
|
||||
@topic.valid?
|
||||
end
|
||||
|
||||
def test_validates_length_of_is_generates_message_with_custom_default_message
|
||||
Topic.validates_length_of :title, :is => 5, :message => 'custom'
|
||||
@topic.errors.expects(:generate_message).with(:title, :wrong_length, {:count => 5, :default => 'custom'})
|
||||
@topic.valid?
|
||||
end
|
||||
|
||||
# validates_uniqueness_of w/ mocha
|
||||
|
||||
def test_validates_uniqueness_of_generates_message
|
||||
Topic.validates_uniqueness_of :title
|
||||
@topic.title = unique_topic.title
|
||||
@topic.errors.expects(:generate_message).with(:title, :taken, {:default => nil, :value => 'unique!'})
|
||||
@topic.valid?
|
||||
end
|
||||
|
||||
def test_validates_uniqueness_of_generates_message_with_custom_default_message
|
||||
Topic.validates_uniqueness_of :title, :message => 'custom'
|
||||
@topic.title = unique_topic.title
|
||||
@topic.errors.expects(:generate_message).with(:title, :taken, {:default => 'custom', :value => 'unique!'})
|
||||
@topic.valid?
|
||||
end
|
||||
|
||||
# validates_format_of w/ mocha
|
||||
|
||||
def test_validates_format_of_generates_message
|
||||
Topic.validates_format_of :title, :with => /^[1-9][0-9]*$/
|
||||
@topic.title = '72x'
|
||||
@topic.errors.expects(:generate_message).with(:title, :invalid, {:value => '72x', :default => nil})
|
||||
@topic.valid?
|
||||
end
|
||||
|
||||
def test_validates_format_of_generates_message_with_custom_default_message
|
||||
Topic.validates_format_of :title, :with => /^[1-9][0-9]*$/, :message => 'custom'
|
||||
@topic.title = '72x'
|
||||
@topic.errors.expects(:generate_message).with(:title, :invalid, {:value => '72x', :default => 'custom'})
|
||||
@topic.valid?
|
||||
end
|
||||
|
||||
# validates_inclusion_of w/ mocha
|
||||
|
||||
def test_validates_inclusion_of_generates_message
|
||||
Topic.validates_inclusion_of :title, :in => %w(a b c)
|
||||
@topic.title = 'z'
|
||||
@topic.errors.expects(:generate_message).with(:title, :inclusion, {:value => 'z', :default => nil})
|
||||
@topic.valid?
|
||||
end
|
||||
|
||||
def test_validates_inclusion_of_generates_message_with_custom_default_message
|
||||
Topic.validates_inclusion_of :title, :in => %w(a b c), :message => 'custom'
|
||||
@topic.title = 'z'
|
||||
@topic.errors.expects(:generate_message).with(:title, :inclusion, {:value => 'z', :default => 'custom'})
|
||||
@topic.valid?
|
||||
end
|
||||
|
||||
# validates_exclusion_of w/ mocha
|
||||
|
||||
def test_validates_exclusion_of_generates_message
|
||||
Topic.validates_exclusion_of :title, :in => %w(a b c)
|
||||
@topic.title = 'a'
|
||||
@topic.errors.expects(:generate_message).with(:title, :exclusion, {:value => 'a', :default => nil})
|
||||
@topic.valid?
|
||||
end
|
||||
|
||||
def test_validates_exclusion_of_generates_message_with_custom_default_message
|
||||
Topic.validates_exclusion_of :title, :in => %w(a b c), :message => 'custom'
|
||||
@topic.title = 'a'
|
||||
@topic.errors.expects(:generate_message).with(:title, :exclusion, {:value => 'a', :default => 'custom'})
|
||||
@topic.valid?
|
||||
end
|
||||
|
||||
# validates_numericality_of without :only_integer w/ mocha
|
||||
|
||||
def test_validates_numericality_of_generates_message
|
||||
Topic.validates_numericality_of :title
|
||||
@topic.title = 'a'
|
||||
@topic.errors.expects(:generate_message).with(:title, :not_a_number, {:value => 'a', :default => nil})
|
||||
@topic.valid?
|
||||
end
|
||||
|
||||
def test_validates_numericality_of_generates_message_with_custom_default_message
|
||||
Topic.validates_numericality_of :title, :message => 'custom'
|
||||
@topic.title = 'a'
|
||||
@topic.errors.expects(:generate_message).with(:title, :not_a_number, {:value => 'a', :default => 'custom'})
|
||||
@topic.valid?
|
||||
end
|
||||
|
||||
# validates_numericality_of with :only_integer w/ mocha
|
||||
|
||||
def test_validates_numericality_of_only_integer_generates_message
|
||||
Topic.validates_numericality_of :title, :only_integer => true
|
||||
@topic.title = 'a'
|
||||
@topic.errors.expects(:generate_message).with(:title, :not_a_number, {:value => 'a', :default => nil})
|
||||
@topic.valid?
|
||||
end
|
||||
|
||||
def test_validates_numericality_of_only_integer_generates_message_with_custom_default_message
|
||||
Topic.validates_numericality_of :title, :only_integer => true, :message => 'custom'
|
||||
@topic.title = 'a'
|
||||
@topic.errors.expects(:generate_message).with(:title, :not_a_number, {:value => 'a', :default => 'custom'})
|
||||
@topic.valid?
|
||||
end
|
||||
|
||||
# validates_numericality_of :odd w/ mocha
|
||||
|
||||
def test_validates_numericality_of_odd_generates_message
|
||||
Topic.validates_numericality_of :title, :only_integer => true, :odd => true
|
||||
@topic.title = 0
|
||||
@topic.errors.expects(:generate_message).with(:title, :odd, {:value => 0, :default => nil})
|
||||
@topic.valid?
|
||||
end
|
||||
|
||||
def test_validates_numericality_of_odd_generates_message_with_custom_default_message
|
||||
Topic.validates_numericality_of :title, :only_integer => true, :odd => true, :message => 'custom'
|
||||
@topic.title = 0
|
||||
@topic.errors.expects(:generate_message).with(:title, :odd, {:value => 0, :default => 'custom'})
|
||||
@topic.valid?
|
||||
end
|
||||
|
||||
# validates_numericality_of :less_than w/ mocha
|
||||
|
||||
def test_validates_numericality_of_less_than_generates_message
|
||||
Topic.validates_numericality_of :title, :only_integer => true, :less_than => 0
|
||||
@topic.title = 1
|
||||
@topic.errors.expects(:generate_message).with(:title, :less_than, {:value => 1, :count => 0, :default => nil})
|
||||
@topic.valid?
|
||||
end
|
||||
|
||||
def test_validates_numericality_of_odd_generates_message_with_custom_default_message
|
||||
Topic.validates_numericality_of :title, :only_integer => true, :less_than => 0, :message => 'custom'
|
||||
@topic.title = 1
|
||||
@topic.errors.expects(:generate_message).with(:title, :less_than, {:value => 1, :count => 0, :default => 'custom'})
|
||||
@topic.valid?
|
||||
end
|
||||
|
||||
# validates_associated w/ mocha
|
||||
|
||||
def test_validates_associated_generates_message
|
||||
Topic.validates_associated :replies
|
||||
replied_topic.errors.expects(:generate_message).with(:replies, :invalid, {:value => replied_topic.replies, :default => nil})
|
||||
replied_topic.valid?
|
||||
end
|
||||
|
||||
def test_validates_associated_generates_message_with_custom_default_message
|
||||
Topic.validates_associated :replies
|
||||
replied_topic.errors.expects(:generate_message).with(:replies, :invalid, {:value => replied_topic.replies, :default => nil})
|
||||
replied_topic.valid?
|
||||
end
|
||||
end
|
||||
|
||||
# validates_confirmation_of w/o mocha
|
||||
|
||||
def test_validates_confirmation_of_finds_custom_model_key_translation
|
||||
I18n.backend.store_translations 'en-US', :activerecord => {:errors => {:models => {:topic => {:attributes => {:title => {:confirmation => 'custom message'}}}}}}
|
||||
I18n.backend.store_translations 'en-US', :activerecord => {:errors => {:messages => {:confirmation => 'global message'}}}
|
||||
|
||||
Topic.validates_confirmation_of :title
|
||||
@topic.title_confirmation = 'foo'
|
||||
@topic.valid?
|
||||
assert_equal 'custom message', @topic.errors.on(:title)
|
||||
end
|
||||
|
||||
def test_validates_confirmation_of_finds_global_default_translation
|
||||
I18n.backend.store_translations 'en-US', :activerecord => {:errors => {:messages => {:confirmation => 'global message'}}}
|
||||
|
||||
Topic.validates_confirmation_of :title
|
||||
@topic.title_confirmation = 'foo'
|
||||
@topic.valid?
|
||||
assert_equal 'global message', @topic.errors.on(:title)
|
||||
end
|
||||
|
||||
# validates_acceptance_of w/o mocha
|
||||
|
||||
def test_validates_acceptance_of_finds_custom_model_key_translation
|
||||
I18n.backend.store_translations 'en-US', :activerecord => {:errors => {:models => {:topic => {:attributes => {:title => {:accepted => 'custom message'}}}}}}
|
||||
I18n.backend.store_translations 'en-US', :activerecord => {:errors => {:messages => {:accepted => 'global message'}}}
|
||||
|
||||
Topic.validates_acceptance_of :title, :allow_nil => false
|
||||
@topic.valid?
|
||||
assert_equal 'custom message', @topic.errors.on(:title)
|
||||
end
|
||||
|
||||
def test_validates_acceptance_of_finds_global_default_translation
|
||||
I18n.backend.store_translations 'en-US', :activerecord => {:errors => {:messages => {:accepted => 'global message'}}}
|
||||
|
||||
Topic.validates_acceptance_of :title, :allow_nil => false
|
||||
@topic.valid?
|
||||
assert_equal 'global message', @topic.errors.on(:title)
|
||||
end
|
||||
|
||||
# validates_presence_of w/o mocha
|
||||
|
||||
def test_validates_presence_of_finds_custom_model_key_translation
|
||||
I18n.backend.store_translations 'en-US', :activerecord => {:errors => {:models => {:topic => {:attributes => {:title => {:blank => 'custom message'}}}}}}
|
||||
I18n.backend.store_translations 'en-US', :activerecord => {:errors => {:messages => {:blank => 'global message'}}}
|
||||
|
||||
Topic.validates_presence_of :title
|
||||
@topic.valid?
|
||||
assert_equal 'custom message', @topic.errors.on(:title)
|
||||
end
|
||||
|
||||
def test_validates_presence_of_finds_global_default_translation
|
||||
I18n.backend.store_translations 'en-US', :activerecord => {:errors => {:messages => {:blank => 'global message'}}}
|
||||
|
||||
Topic.validates_presence_of :title
|
||||
@topic.valid?
|
||||
assert_equal 'global message', @topic.errors.on(:title)
|
||||
end
|
||||
|
||||
# validates_length_of :within w/o mocha
|
||||
|
||||
def test_validates_length_of_within_finds_custom_model_key_translation
|
||||
I18n.backend.store_translations 'en-US', :activerecord => {:errors => {:models => {:topic => {:attributes => {:title => {:too_short => 'custom message'}}}}}}
|
||||
I18n.backend.store_translations 'en-US', :activerecord => {:errors => {:messages => {:too_short => 'global message'}}}
|
||||
|
||||
Topic.validates_length_of :title, :within => 3..5
|
||||
@topic.valid?
|
||||
assert_equal 'custom message', @topic.errors.on(:title)
|
||||
end
|
||||
|
||||
def test_validates_length_of_within_finds_global_default_translation
|
||||
I18n.backend.store_translations 'en-US', :activerecord => {:errors => {:messages => {:too_short => 'global message'}}}
|
||||
|
||||
Topic.validates_length_of :title, :within => 3..5
|
||||
@topic.valid?
|
||||
assert_equal 'global message', @topic.errors.on(:title)
|
||||
end
|
||||
|
||||
# validates_length_of :is w/o mocha
|
||||
|
||||
def test_validates_length_of_within_finds_custom_model_key_translation
|
||||
I18n.backend.store_translations 'en-US', :activerecord => {:errors => {:models => {:topic => {:attributes => {:title => {:wrong_length => 'custom message'}}}}}}
|
||||
I18n.backend.store_translations 'en-US', :activerecord => {:errors => {:messages => {:wrong_length => 'global message'}}}
|
||||
|
||||
Topic.validates_length_of :title, :is => 5
|
||||
@topic.valid?
|
||||
assert_equal 'custom message', @topic.errors.on(:title)
|
||||
end
|
||||
|
||||
def test_validates_length_of_within_finds_global_default_translation
|
||||
I18n.backend.store_translations 'en-US', :activerecord => {:errors => {:messages => {:wrong_length => 'global message'}}}
|
||||
|
||||
Topic.validates_length_of :title, :is => 5
|
||||
@topic.valid?
|
||||
assert_equal 'global message', @topic.errors.on(:title)
|
||||
end
|
||||
|
||||
# validates_uniqueness_of w/o mocha
|
||||
|
||||
def test_validates_length_of_within_finds_custom_model_key_translation
|
||||
I18n.backend.store_translations 'en-US', :activerecord => {:errors => {:models => {:topic => {:attributes => {:title => {:wrong_length => 'custom message'}}}}}}
|
||||
I18n.backend.store_translations 'en-US', :activerecord => {:errors => {:messages => {:wrong_length => 'global message'}}}
|
||||
|
||||
Topic.validates_length_of :title, :is => 5
|
||||
@topic.valid?
|
||||
assert_equal 'custom message', @topic.errors.on(:title)
|
||||
end
|
||||
|
||||
def test_validates_length_of_within_finds_global_default_translation
|
||||
I18n.backend.store_translations 'en-US', :activerecord => {:errors => {:messages => {:wrong_length => 'global message'}}}
|
||||
|
||||
Topic.validates_length_of :title, :is => 5
|
||||
@topic.valid?
|
||||
assert_equal 'global message', @topic.errors.on(:title)
|
||||
end
|
||||
|
||||
|
||||
# validates_format_of w/o mocha
|
||||
|
||||
def test_validates_format_of_finds_custom_model_key_translation
|
||||
I18n.backend.store_translations 'en-US', :activerecord => {:errors => {:models => {:topic => {:attributes => {:title => {:invalid => 'custom message'}}}}}}
|
||||
I18n.backend.store_translations 'en-US', :activerecord => {:errors => {:messages => {:invalid => 'global message'}}}
|
||||
|
||||
Topic.validates_format_of :title, :with => /^[1-9][0-9]*$/
|
||||
@topic.valid?
|
||||
assert_equal 'custom message', @topic.errors.on(:title)
|
||||
end
|
||||
|
||||
def test_validates_format_of_finds_global_default_translation
|
||||
I18n.backend.store_translations 'en-US', :activerecord => {:errors => {:messages => {:invalid => 'global message'}}}
|
||||
|
||||
Topic.validates_format_of :title, :with => /^[1-9][0-9]*$/
|
||||
@topic.valid?
|
||||
assert_equal 'global message', @topic.errors.on(:title)
|
||||
end
|
||||
|
||||
# validates_inclusion_of w/o mocha
|
||||
|
||||
def test_validates_inclusion_of_finds_custom_model_key_translation
|
||||
I18n.backend.store_translations 'en-US', :activerecord => {:errors => {:models => {:topic => {:attributes => {:title => {:inclusion => 'custom message'}}}}}}
|
||||
I18n.backend.store_translations 'en-US', :activerecord => {:errors => {:messages => {:inclusion => 'global message'}}}
|
||||
|
||||
Topic.validates_inclusion_of :title, :in => %w(a b c)
|
||||
@topic.valid?
|
||||
assert_equal 'custom message', @topic.errors.on(:title)
|
||||
end
|
||||
|
||||
def test_validates_inclusion_of_finds_global_default_translation
|
||||
I18n.backend.store_translations 'en-US', :activerecord => {:errors => {:messages => {:inclusion => 'global message'}}}
|
||||
|
||||
Topic.validates_inclusion_of :title, :in => %w(a b c)
|
||||
@topic.valid?
|
||||
assert_equal 'global message', @topic.errors.on(:title)
|
||||
end
|
||||
|
||||
# validates_exclusion_of w/o mocha
|
||||
|
||||
def test_validates_exclusion_of_finds_custom_model_key_translation
|
||||
I18n.backend.store_translations 'en-US', :activerecord => {:errors => {:models => {:topic => {:attributes => {:title => {:exclusion => 'custom message'}}}}}}
|
||||
I18n.backend.store_translations 'en-US', :activerecord => {:errors => {:messages => {:exclusion => 'global message'}}}
|
||||
|
||||
Topic.validates_exclusion_of :title, :in => %w(a b c)
|
||||
@topic.title = 'a'
|
||||
@topic.valid?
|
||||
assert_equal 'custom message', @topic.errors.on(:title)
|
||||
end
|
||||
|
||||
def test_validates_exclusion_of_finds_global_default_translation
|
||||
I18n.backend.store_translations 'en-US', :activerecord => {:errors => {:messages => {:exclusion => 'global message'}}}
|
||||
|
||||
Topic.validates_exclusion_of :title, :in => %w(a b c)
|
||||
@topic.title = 'a'
|
||||
@topic.valid?
|
||||
assert_equal 'global message', @topic.errors.on(:title)
|
||||
end
|
||||
|
||||
# validates_numericality_of without :only_integer w/o mocha
|
||||
|
||||
def test_validates_numericality_of_finds_custom_model_key_translation
|
||||
I18n.backend.store_translations 'en-US', :activerecord => {:errors => {:models => {:topic => {:attributes => {:title => {:not_a_number => 'custom message'}}}}}}
|
||||
I18n.backend.store_translations 'en-US', :activerecord => {:errors => {:messages => {:not_a_number => 'global message'}}}
|
||||
|
||||
Topic.validates_numericality_of :title
|
||||
@topic.title = 'a'
|
||||
@topic.valid?
|
||||
assert_equal 'custom message', @topic.errors.on(:title)
|
||||
end
|
||||
|
||||
def test_validates_numericality_of_finds_global_default_translation
|
||||
I18n.backend.store_translations 'en-US', :activerecord => {:errors => {:messages => {:not_a_number => 'global message'}}}
|
||||
|
||||
Topic.validates_numericality_of :title, :only_integer => true
|
||||
@topic.title = 'a'
|
||||
@topic.valid?
|
||||
assert_equal 'global message', @topic.errors.on(:title)
|
||||
end
|
||||
|
||||
# validates_numericality_of with :only_integer w/o mocha
|
||||
|
||||
def test_validates_numericality_of_only_integer_finds_custom_model_key_translation
|
||||
I18n.backend.store_translations 'en-US', :activerecord => {:errors => {:models => {:topic => {:attributes => {:title => {:not_a_number => 'custom message'}}}}}}
|
||||
I18n.backend.store_translations 'en-US', :activerecord => {:errors => {:messages => {:not_a_number => 'global message'}}}
|
||||
|
||||
Topic.validates_numericality_of :title, :only_integer => true
|
||||
@topic.title = 'a'
|
||||
@topic.valid?
|
||||
assert_equal 'custom message', @topic.errors.on(:title)
|
||||
end
|
||||
|
||||
def test_validates_numericality_of_only_integer_finds_global_default_translation
|
||||
I18n.backend.store_translations 'en-US', :activerecord => {:errors => {:messages => {:not_a_number => 'global message'}}}
|
||||
|
||||
Topic.validates_numericality_of :title, :only_integer => true
|
||||
@topic.title = 'a'
|
||||
@topic.valid?
|
||||
assert_equal 'global message', @topic.errors.on(:title)
|
||||
end
|
||||
|
||||
# validates_numericality_of :odd w/o mocha
|
||||
|
||||
def test_validates_numericality_of_odd_finds_custom_model_key_translation
|
||||
I18n.backend.store_translations 'en-US', :activerecord => {:errors => {:models => {:topic => {:attributes => {:title => {:odd => 'custom message'}}}}}}
|
||||
I18n.backend.store_translations 'en-US', :activerecord => {:errors => {:messages => {:odd => 'global message'}}}
|
||||
|
||||
Topic.validates_numericality_of :title, :only_integer => true, :odd => true
|
||||
@topic.title = 0
|
||||
@topic.valid?
|
||||
assert_equal 'custom message', @topic.errors.on(:title)
|
||||
end
|
||||
|
||||
def test_validates_numericality_of_odd_finds_global_default_translation
|
||||
I18n.backend.store_translations 'en-US', :activerecord => {:errors => {:messages => {:odd => 'global message'}}}
|
||||
|
||||
Topic.validates_numericality_of :title, :only_integer => true, :odd => true
|
||||
@topic.title = 0
|
||||
@topic.valid?
|
||||
assert_equal 'global message', @topic.errors.on(:title)
|
||||
end
|
||||
|
||||
# validates_numericality_of :less_than w/o mocha
|
||||
|
||||
def test_validates_numericality_of_less_than_finds_custom_model_key_translation
|
||||
I18n.backend.store_translations 'en-US', :activerecord => {:errors => {:models => {:topic => {:attributes => {:title => {:less_than => 'custom message'}}}}}}
|
||||
I18n.backend.store_translations 'en-US', :activerecord => {:errors => {:messages => {:less_than => 'global message'}}}
|
||||
|
||||
Topic.validates_numericality_of :title, :only_integer => true, :less_than => 0
|
||||
@topic.title = 1
|
||||
@topic.valid?
|
||||
assert_equal 'custom message', @topic.errors.on(:title)
|
||||
end
|
||||
|
||||
def test_validates_numericality_of_less_than_finds_global_default_translation
|
||||
I18n.backend.store_translations 'en-US', :activerecord => {:errors => {:messages => {:less_than => 'global message'}}}
|
||||
|
||||
Topic.validates_numericality_of :title, :only_integer => true, :less_than => 0
|
||||
@topic.title = 1
|
||||
@topic.valid?
|
||||
assert_equal 'global message', @topic.errors.on(:title)
|
||||
end
|
||||
|
||||
|
||||
# validates_associated w/o mocha
|
||||
|
||||
def test_validates_associated_finds_custom_model_key_translation
|
||||
I18n.backend.store_translations 'en-US', :activerecord => {:errors => {:models => {:topic => {:attributes => {:replies => {:invalid => 'custom message'}}}}}}
|
||||
I18n.backend.store_translations 'en-US', :activerecord => {:errors => {:messages => {:invalid => 'global message'}}}
|
||||
|
||||
Topic.validates_associated :replies
|
||||
replied_topic.valid?
|
||||
assert_equal 'custom message', replied_topic.errors.on(:replies)
|
||||
end
|
||||
|
||||
def test_validates_associated_finds_global_default_translation
|
||||
I18n.backend.store_translations 'en-US', :activerecord => {:errors => {:messages => {:invalid => 'global message'}}}
|
||||
|
||||
Topic.validates_associated :replies
|
||||
replied_topic.valid?
|
||||
assert_equal 'global message', replied_topic.errors.on(:replies)
|
||||
end
|
||||
|
||||
def test_validations_with_message_symbol_must_translate
|
||||
I18n.backend.store_translations 'en-US', :activerecord => {:errors => {:messages => {:custom_error => "I am a custom error"}}}
|
||||
Topic.validates_presence_of :title, :message => :custom_error
|
||||
@topic.title = nil
|
||||
@topic.valid?
|
||||
assert_equal "I am a custom error", @topic.errors.on(:title)
|
||||
end
|
||||
|
||||
def test_validates_with_message_symbol_must_translate_per_attribute
|
||||
I18n.backend.store_translations 'en-US', :activerecord => {:errors => {:models => {:topic => {:attributes => {:title => {:custom_error => "I am a custom error"}}}}}}
|
||||
Topic.validates_presence_of :title, :message => :custom_error
|
||||
@topic.title = nil
|
||||
@topic.valid?
|
||||
assert_equal "I am a custom error", @topic.errors.on(:title)
|
||||
end
|
||||
|
||||
def test_validates_with_message_symbol_must_translate_per_model
|
||||
I18n.backend.store_translations 'en-US', :activerecord => {:errors => {:models => {:topic => {:custom_error => "I am a custom error"}}}}
|
||||
Topic.validates_presence_of :title, :message => :custom_error
|
||||
@topic.title = nil
|
||||
@topic.valid?
|
||||
assert_equal "I am a custom error", @topic.errors.on(:title)
|
||||
end
|
||||
|
||||
def test_validates_with_message_string
|
||||
Topic.validates_presence_of :title, :message => "I am a custom error"
|
||||
@topic.title = nil
|
||||
@topic.valid?
|
||||
assert_equal "I am a custom error", @topic.errors.on(:title)
|
||||
end
|
||||
|
||||
end
|
||||
|
||||
class ActiveRecordValidationsGenerateMessageI18nTests < Test::Unit::TestCase
|
||||
def setup
|
||||
reset_callbacks Topic
|
||||
@topic = Topic.new
|
||||
I18n.backend.store_translations :'en-US', {
|
||||
:activerecord => {
|
||||
:errors => {
|
||||
: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"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
end
|
||||
|
||||
def reset_callbacks(*models)
|
||||
models.each do |model|
|
||||
model.instance_variable_set("@validate_callbacks", ActiveSupport::Callbacks::CallbackChain.new)
|
||||
model.instance_variable_set("@validate_on_create_callbacks", ActiveSupport::Callbacks::CallbackChain.new)
|
||||
model.instance_variable_set("@validate_on_update_callbacks", ActiveSupport::Callbacks::CallbackChain.new)
|
||||
end
|
||||
end
|
||||
|
||||
# validates_inclusion_of: generate_message(attr_name, :inclusion, :default => configuration[:message], :value => value)
|
||||
def test_generate_message_inclusion_with_default_message
|
||||
assert_equal 'is not included in the list', @topic.errors.generate_message(:title, :inclusion, :default => nil, :value => 'title')
|
||||
end
|
||||
|
||||
def test_generate_message_inclusion_with_custom_message
|
||||
assert_equal 'custom message title', @topic.errors.generate_message(:title, :inclusion, :default => 'custom message {{value}}', :value => 'title')
|
||||
end
|
||||
|
||||
# validates_exclusion_of: generate_message(attr_name, :exclusion, :default => configuration[:message], :value => value)
|
||||
def test_generate_message_exclusion_with_default_message
|
||||
assert_equal 'is reserved', @topic.errors.generate_message(:title, :exclusion, :default => nil, :value => 'title')
|
||||
end
|
||||
|
||||
def test_generate_message_exclusion_with_custom_message
|
||||
assert_equal 'custom message title', @topic.errors.generate_message(:title, :exclusion, :default => 'custom message {{value}}', :value => 'title')
|
||||
end
|
||||
|
||||
# validates_associated: generate_message(attr_name, :invalid, :default => configuration[:message], :value => value)
|
||||
# validates_format_of: generate_message(attr_name, :invalid, :default => configuration[:message], :value => value)
|
||||
def test_generate_message_invalid_with_default_message
|
||||
assert_equal 'is invalid', @topic.errors.generate_message(:title, :invalid, :default => nil, :value => 'title')
|
||||
end
|
||||
|
||||
def test_generate_message_invalid_with_custom_message
|
||||
assert_equal 'custom message title', @topic.errors.generate_message(:title, :invalid, :default => 'custom message {{value}}', :value => 'title')
|
||||
end
|
||||
|
||||
# validates_confirmation_of: generate_message(attr_name, :confirmation, :default => configuration[:message])
|
||||
def test_generate_message_confirmation_with_default_message
|
||||
assert_equal "doesn't match confirmation", @topic.errors.generate_message(:title, :confirmation, :default => nil)
|
||||
end
|
||||
|
||||
def test_generate_message_confirmation_with_custom_message
|
||||
assert_equal 'custom message', @topic.errors.generate_message(:title, :confirmation, :default => 'custom message')
|
||||
end
|
||||
|
||||
# validates_acceptance_of: generate_message(attr_name, :accepted, :default => configuration[:message])
|
||||
def test_generate_message_accepted_with_default_message
|
||||
assert_equal "must be accepted", @topic.errors.generate_message(:title, :accepted, :default => nil)
|
||||
end
|
||||
|
||||
def test_generate_message_accepted_with_custom_message
|
||||
assert_equal 'custom message', @topic.errors.generate_message(:title, :accepted, :default => 'custom message')
|
||||
end
|
||||
|
||||
# add_on_empty: generate_message(attr, :empty, :default => custom_message)
|
||||
def test_generate_message_empty_with_default_message
|
||||
assert_equal "can't be empty", @topic.errors.generate_message(:title, :empty, :default => nil)
|
||||
end
|
||||
|
||||
def test_generate_message_empty_with_custom_message
|
||||
assert_equal 'custom message', @topic.errors.generate_message(:title, :empty, :default => 'custom message')
|
||||
end
|
||||
|
||||
# add_on_blank: generate_message(attr, :blank, :default => custom_message)
|
||||
def test_generate_message_blank_with_default_message
|
||||
assert_equal "can't be blank", @topic.errors.generate_message(:title, :blank, :default => nil)
|
||||
end
|
||||
|
||||
def test_generate_message_blank_with_custom_message
|
||||
assert_equal 'custom message', @topic.errors.generate_message(:title, :blank, :default => 'custom message')
|
||||
end
|
||||
|
||||
# validates_length_of: generate_message(attr, :too_long, :default => options[:too_long], :count => option_value.end)
|
||||
def test_generate_message_too_long_with_default_message
|
||||
assert_equal "is too long (maximum is 10 characters)", @topic.errors.generate_message(:title, :too_long, :default => nil, :count => 10)
|
||||
end
|
||||
|
||||
def test_generate_message_too_long_with_custom_message
|
||||
assert_equal 'custom message 10', @topic.errors.generate_message(:title, :too_long, :default => 'custom message {{count}}', :count => 10)
|
||||
end
|
||||
|
||||
# validates_length_of: generate_message(attr, :too_short, :default => options[:too_short], :count => option_value.begin)
|
||||
def test_generate_message_too_short_with_default_message
|
||||
assert_equal "is too short (minimum is 10 characters)", @topic.errors.generate_message(:title, :too_short, :default => nil, :count => 10)
|
||||
end
|
||||
|
||||
def test_generate_message_too_short_with_custom_message
|
||||
assert_equal 'custom message 10', @topic.errors.generate_message(:title, :too_short, :default => 'custom message {{count}}', :count => 10)
|
||||
end
|
||||
|
||||
# validates_length_of: generate_message(attr, key, :default => custom_message, :count => option_value)
|
||||
def test_generate_message_wrong_length_with_default_message
|
||||
assert_equal "is the wrong length (should be 10 characters)", @topic.errors.generate_message(:title, :wrong_length, :default => nil, :count => 10)
|
||||
end
|
||||
|
||||
def test_generate_message_wrong_length_with_custom_message
|
||||
assert_equal 'custom message 10', @topic.errors.generate_message(:title, :wrong_length, :default => 'custom message {{count}}', :count => 10)
|
||||
end
|
||||
|
||||
# validates_uniqueness_of: generate_message(attr_name, :taken, :default => configuration[:message])
|
||||
def test_generate_message_taken_with_default_message
|
||||
assert_equal "has already been taken", @topic.errors.generate_message(:title, :taken, :default => nil, :value => 'title')
|
||||
end
|
||||
|
||||
def test_generate_message_taken_with_custom_message
|
||||
assert_equal 'custom message title', @topic.errors.generate_message(:title, :taken, :default => 'custom message {{value}}', :value => 'title')
|
||||
end
|
||||
|
||||
# validates_numericality_of: generate_message(attr_name, :not_a_number, :value => raw_value, :default => configuration[:message])
|
||||
def test_generate_message_not_a_number_with_default_message
|
||||
assert_equal "is not a number", @topic.errors.generate_message(:title, :not_a_number, :default => nil, :value => 'title')
|
||||
end
|
||||
|
||||
def test_generate_message_not_a_number_with_custom_message
|
||||
assert_equal 'custom message title', @topic.errors.generate_message(:title, :not_a_number, :default => 'custom message {{value}}', :value => 'title')
|
||||
end
|
||||
|
||||
# validates_numericality_of: generate_message(attr_name, option, :value => raw_value, :default => configuration[:message])
|
||||
def test_generate_message_greater_than_with_default_message
|
||||
assert_equal "must be greater than 10", @topic.errors.generate_message(:title, :greater_than, :default => nil, :value => 'title', :count => 10)
|
||||
end
|
||||
|
||||
def test_generate_message_greater_than_or_equal_to_with_default_message
|
||||
assert_equal "must be greater than or equal to 10", @topic.errors.generate_message(:title, :greater_than_or_equal_to, :default => nil, :value => 'title', :count => 10)
|
||||
end
|
||||
|
||||
def test_generate_message_equal_to_with_default_message
|
||||
assert_equal "must be equal to 10", @topic.errors.generate_message(:title, :equal_to, :default => nil, :value => 'title', :count => 10)
|
||||
end
|
||||
|
||||
def test_generate_message_less_than_with_default_message
|
||||
assert_equal "must be less than 10", @topic.errors.generate_message(:title, :less_than, :default => nil, :value => 'title', :count => 10)
|
||||
end
|
||||
|
||||
def test_generate_message_less_than_or_equal_to_with_default_message
|
||||
assert_equal "must be less than or equal to 10", @topic.errors.generate_message(:title, :less_than_or_equal_to, :default => nil, :value => 'title', :count => 10)
|
||||
end
|
||||
|
||||
def test_generate_message_odd_with_default_message
|
||||
assert_equal "must be odd", @topic.errors.generate_message(:title, :odd, :default => nil, :value => 'title', :count => 10)
|
||||
end
|
||||
|
||||
def test_generate_message_even_with_default_message
|
||||
assert_equal "must be even", @topic.errors.generate_message(:title, :even, :default => nil, :value => 'title', :count => 10)
|
||||
end
|
||||
|
||||
end
|
|
@ -364,6 +364,13 @@ class ValidationsTest < ActiveRecord::TestCase
|
|||
assert t2.save, "Should now save t2 as unique"
|
||||
end
|
||||
|
||||
def test_validates_uniquness_with_newline_chars
|
||||
Topic.validates_uniqueness_of(:title, :case_sensitive => false)
|
||||
|
||||
t = Topic.new("title" => "new\nline")
|
||||
assert t.save, "Should save t as unique"
|
||||
end
|
||||
|
||||
def test_validate_uniqueness_with_scope
|
||||
Reply.validates_uniqueness_of(:content, :scope => "parent_id")
|
||||
|
||||
|
@ -489,6 +496,15 @@ class ValidationsTest < ActiveRecord::TestCase
|
|||
assert_not_equal "has already been taken", t3.errors.on(:title)
|
||||
end
|
||||
|
||||
def test_validate_case_sensitive_uniqueness_with_attribute_passed_as_integer
|
||||
Topic.validates_uniqueness_of(:title, :case_sensitve => true)
|
||||
t = Topic.create!('title' => 101)
|
||||
|
||||
t2 = Topic.new('title' => 101)
|
||||
assert !t2.valid?
|
||||
assert t2.errors.on(:title)
|
||||
end
|
||||
|
||||
def test_validate_uniqueness_with_non_standard_table_names
|
||||
i1 = WarehouseThing.create(:value => 1000)
|
||||
assert !i1.valid?, "i1 should not be valid"
|
||||
|
@ -596,7 +612,7 @@ class ValidationsTest < ActiveRecord::TestCase
|
|||
end
|
||||
|
||||
def test_validate_format_with_formatted_message
|
||||
Topic.validates_format_of(:title, :with => /^Valid Title$/, :message => "can't be %s")
|
||||
Topic.validates_format_of(:title, :with => /^Valid Title$/, :message => "can't be {{value}}")
|
||||
t = Topic.create(:title => 'Invalid title')
|
||||
assert_equal "can't be Invalid title", t.errors.on(:title)
|
||||
end
|
||||
|
@ -657,7 +673,7 @@ class ValidationsTest < ActiveRecord::TestCase
|
|||
end
|
||||
|
||||
def test_validates_inclusion_of_with_formatted_message
|
||||
Topic.validates_inclusion_of( :title, :in => %w( a b c d e f g ), :message => "option %s is not in the list" )
|
||||
Topic.validates_inclusion_of( :title, :in => %w( a b c d e f g ), :message => "option {{value}} is not in the list" )
|
||||
|
||||
assert Topic.create("title" => "a", "content" => "abc").valid?
|
||||
|
||||
|
@ -682,7 +698,7 @@ class ValidationsTest < ActiveRecord::TestCase
|
|||
end
|
||||
|
||||
def test_validates_exclusion_of_with_formatted_message
|
||||
Topic.validates_exclusion_of( :title, :in => %w( abe monkey ), :message => "option %s is restricted" )
|
||||
Topic.validates_exclusion_of( :title, :in => %w( abe monkey ), :message => "option {{value}} is restricted" )
|
||||
|
||||
assert Topic.create("title" => "something", "content" => "abc")
|
||||
|
||||
|
@ -782,7 +798,7 @@ class ValidationsTest < ActiveRecord::TestCase
|
|||
end
|
||||
|
||||
def test_optionally_validates_length_of_using_within_on_create
|
||||
Topic.validates_length_of :title, :content, :within => 5..10, :on => :create, :too_long => "my string is too long: %d"
|
||||
Topic.validates_length_of :title, :content, :within => 5..10, :on => :create, :too_long => "my string is too long: {{count}}"
|
||||
|
||||
t = Topic.create("title" => "thisisnotvalid", "content" => "whatever")
|
||||
assert !t.save
|
||||
|
@ -803,7 +819,7 @@ class ValidationsTest < ActiveRecord::TestCase
|
|||
end
|
||||
|
||||
def test_optionally_validates_length_of_using_within_on_update
|
||||
Topic.validates_length_of :title, :content, :within => 5..10, :on => :update, :too_short => "my string is too short: %d"
|
||||
Topic.validates_length_of :title, :content, :within => 5..10, :on => :update, :too_short => "my string is too short: {{count}}"
|
||||
|
||||
t = Topic.create("title" => "vali", "content" => "whatever")
|
||||
assert !t.save
|
||||
|
@ -865,7 +881,9 @@ class ValidationsTest < ActiveRecord::TestCase
|
|||
end
|
||||
|
||||
def test_validates_length_with_globally_modified_error_message
|
||||
ActiveRecord::Errors.default_error_messages[:too_short] = 'tu est trops petit hombre %d'
|
||||
ActiveSupport::Deprecation.silence do
|
||||
ActiveRecord::Errors.default_error_messages[:too_short] = 'tu est trops petit hombre {{count}}'
|
||||
end
|
||||
Topic.validates_length_of :title, :minimum => 10
|
||||
t = Topic.create(:title => 'too short')
|
||||
assert !t.valid?
|
||||
|
@ -908,7 +926,7 @@ class ValidationsTest < ActiveRecord::TestCase
|
|||
end
|
||||
|
||||
def test_validates_length_of_custom_errors_for_minimum_with_message
|
||||
Topic.validates_length_of( :title, :minimum=>5, :message=>"boo %d" )
|
||||
Topic.validates_length_of( :title, :minimum=>5, :message=>"boo {{count}}" )
|
||||
t = Topic.create("title" => "uhoh", "content" => "whatever")
|
||||
assert !t.valid?
|
||||
assert t.errors.on(:title)
|
||||
|
@ -916,7 +934,7 @@ class ValidationsTest < ActiveRecord::TestCase
|
|||
end
|
||||
|
||||
def test_validates_length_of_custom_errors_for_minimum_with_too_short
|
||||
Topic.validates_length_of( :title, :minimum=>5, :too_short=>"hoo %d" )
|
||||
Topic.validates_length_of( :title, :minimum=>5, :too_short=>"hoo {{count}}" )
|
||||
t = Topic.create("title" => "uhoh", "content" => "whatever")
|
||||
assert !t.valid?
|
||||
assert t.errors.on(:title)
|
||||
|
@ -924,7 +942,7 @@ class ValidationsTest < ActiveRecord::TestCase
|
|||
end
|
||||
|
||||
def test_validates_length_of_custom_errors_for_maximum_with_message
|
||||
Topic.validates_length_of( :title, :maximum=>5, :message=>"boo %d" )
|
||||
Topic.validates_length_of( :title, :maximum=>5, :message=>"boo {{count}}" )
|
||||
t = Topic.create("title" => "uhohuhoh", "content" => "whatever")
|
||||
assert !t.valid?
|
||||
assert t.errors.on(:title)
|
||||
|
@ -932,7 +950,7 @@ class ValidationsTest < ActiveRecord::TestCase
|
|||
end
|
||||
|
||||
def test_validates_length_of_custom_errors_for_maximum_with_too_long
|
||||
Topic.validates_length_of( :title, :maximum=>5, :too_long=>"hoo %d" )
|
||||
Topic.validates_length_of( :title, :maximum=>5, :too_long=>"hoo {{count}}" )
|
||||
t = Topic.create("title" => "uhohuhoh", "content" => "whatever")
|
||||
assert !t.valid?
|
||||
assert t.errors.on(:title)
|
||||
|
@ -940,7 +958,7 @@ class ValidationsTest < ActiveRecord::TestCase
|
|||
end
|
||||
|
||||
def test_validates_length_of_custom_errors_for_is_with_message
|
||||
Topic.validates_length_of( :title, :is=>5, :message=>"boo %d" )
|
||||
Topic.validates_length_of( :title, :is=>5, :message=>"boo {{count}}" )
|
||||
t = Topic.create("title" => "uhohuhoh", "content" => "whatever")
|
||||
assert !t.valid?
|
||||
assert t.errors.on(:title)
|
||||
|
@ -948,7 +966,7 @@ class ValidationsTest < ActiveRecord::TestCase
|
|||
end
|
||||
|
||||
def test_validates_length_of_custom_errors_for_is_with_wrong_length
|
||||
Topic.validates_length_of( :title, :is=>5, :wrong_length=>"hoo %d" )
|
||||
Topic.validates_length_of( :title, :is=>5, :wrong_length=>"hoo {{count}}" )
|
||||
t = Topic.create("title" => "uhohuhoh", "content" => "whatever")
|
||||
assert !t.valid?
|
||||
assert t.errors.on(:title)
|
||||
|
@ -1014,7 +1032,7 @@ class ValidationsTest < ActiveRecord::TestCase
|
|||
|
||||
def test_optionally_validates_length_of_using_within_on_create_utf8
|
||||
with_kcode('UTF8') do
|
||||
Topic.validates_length_of :title, :within => 5..10, :on => :create, :too_long => "長すぎます: %d"
|
||||
Topic.validates_length_of :title, :within => 5..10, :on => :create, :too_long => "長すぎます: {{count}}"
|
||||
|
||||
t = Topic.create("title" => "一二三四五六七八九十A", "content" => "whatever")
|
||||
assert !t.save
|
||||
|
@ -1037,7 +1055,7 @@ class ValidationsTest < ActiveRecord::TestCase
|
|||
|
||||
def test_optionally_validates_length_of_using_within_on_update_utf8
|
||||
with_kcode('UTF8') do
|
||||
Topic.validates_length_of :title, :within => 5..10, :on => :update, :too_short => "短すぎます: %d"
|
||||
Topic.validates_length_of :title, :within => 5..10, :on => :update, :too_short => "短すぎます: {{count}}"
|
||||
|
||||
t = Topic.create("title" => "一二三4", "content" => "whatever")
|
||||
assert !t.save
|
||||
|
@ -1072,7 +1090,7 @@ class ValidationsTest < ActiveRecord::TestCase
|
|||
end
|
||||
|
||||
def test_validates_length_of_with_block
|
||||
Topic.validates_length_of :content, :minimum => 5, :too_short=>"Your essay must be at least %d words.",
|
||||
Topic.validates_length_of :content, :minimum => 5, :too_short=>"Your essay must be at least {{count}} words.",
|
||||
:tokenizer => lambda {|str| str.scan(/\w+/) }
|
||||
t = Topic.create!(:content => "this content should be long enough")
|
||||
assert t.valid?
|
||||
|
@ -1223,7 +1241,7 @@ class ValidationsTest < ActiveRecord::TestCase
|
|||
|
||||
def test_if_validation_using_method_true
|
||||
# When the method returns true
|
||||
Topic.validates_length_of( :title, :maximum=>5, :too_long=>"hoo %d", :if => :condition_is_true )
|
||||
Topic.validates_length_of( :title, :maximum=>5, :too_long=>"hoo {{count}}", :if => :condition_is_true )
|
||||
t = Topic.create("title" => "uhohuhoh", "content" => "whatever")
|
||||
assert !t.valid?
|
||||
assert t.errors.on(:title)
|
||||
|
@ -1232,7 +1250,7 @@ class ValidationsTest < ActiveRecord::TestCase
|
|||
|
||||
def test_unless_validation_using_method_true
|
||||
# When the method returns true
|
||||
Topic.validates_length_of( :title, :maximum=>5, :too_long=>"hoo %d", :unless => :condition_is_true )
|
||||
Topic.validates_length_of( :title, :maximum=>5, :too_long=>"hoo {{count}}", :unless => :condition_is_true )
|
||||
t = Topic.create("title" => "uhohuhoh", "content" => "whatever")
|
||||
assert t.valid?
|
||||
assert !t.errors.on(:title)
|
||||
|
@ -1240,7 +1258,7 @@ class ValidationsTest < ActiveRecord::TestCase
|
|||
|
||||
def test_if_validation_using_method_false
|
||||
# When the method returns false
|
||||
Topic.validates_length_of( :title, :maximum=>5, :too_long=>"hoo %d", :if => :condition_is_true_but_its_not )
|
||||
Topic.validates_length_of( :title, :maximum=>5, :too_long=>"hoo {{count}}", :if => :condition_is_true_but_its_not )
|
||||
t = Topic.create("title" => "uhohuhoh", "content" => "whatever")
|
||||
assert t.valid?
|
||||
assert !t.errors.on(:title)
|
||||
|
@ -1248,7 +1266,7 @@ class ValidationsTest < ActiveRecord::TestCase
|
|||
|
||||
def test_unless_validation_using_method_false
|
||||
# When the method returns false
|
||||
Topic.validates_length_of( :title, :maximum=>5, :too_long=>"hoo %d", :unless => :condition_is_true_but_its_not )
|
||||
Topic.validates_length_of( :title, :maximum=>5, :too_long=>"hoo {{count}}", :unless => :condition_is_true_but_its_not )
|
||||
t = Topic.create("title" => "uhohuhoh", "content" => "whatever")
|
||||
assert !t.valid?
|
||||
assert t.errors.on(:title)
|
||||
|
@ -1257,7 +1275,7 @@ class ValidationsTest < ActiveRecord::TestCase
|
|||
|
||||
def test_if_validation_using_string_true
|
||||
# When the evaluated string returns true
|
||||
Topic.validates_length_of( :title, :maximum=>5, :too_long=>"hoo %d", :if => "a = 1; a == 1" )
|
||||
Topic.validates_length_of( :title, :maximum=>5, :too_long=>"hoo {{count}}", :if => "a = 1; a == 1" )
|
||||
t = Topic.create("title" => "uhohuhoh", "content" => "whatever")
|
||||
assert !t.valid?
|
||||
assert t.errors.on(:title)
|
||||
|
@ -1266,7 +1284,7 @@ class ValidationsTest < ActiveRecord::TestCase
|
|||
|
||||
def test_unless_validation_using_string_true
|
||||
# When the evaluated string returns true
|
||||
Topic.validates_length_of( :title, :maximum=>5, :too_long=>"hoo %d", :unless => "a = 1; a == 1" )
|
||||
Topic.validates_length_of( :title, :maximum=>5, :too_long=>"hoo {{count}}", :unless => "a = 1; a == 1" )
|
||||
t = Topic.create("title" => "uhohuhoh", "content" => "whatever")
|
||||
assert t.valid?
|
||||
assert !t.errors.on(:title)
|
||||
|
@ -1274,7 +1292,7 @@ class ValidationsTest < ActiveRecord::TestCase
|
|||
|
||||
def test_if_validation_using_string_false
|
||||
# When the evaluated string returns false
|
||||
Topic.validates_length_of( :title, :maximum=>5, :too_long=>"hoo %d", :if => "false")
|
||||
Topic.validates_length_of( :title, :maximum=>5, :too_long=>"hoo {{count}}", :if => "false")
|
||||
t = Topic.create("title" => "uhohuhoh", "content" => "whatever")
|
||||
assert t.valid?
|
||||
assert !t.errors.on(:title)
|
||||
|
@ -1282,7 +1300,7 @@ class ValidationsTest < ActiveRecord::TestCase
|
|||
|
||||
def test_unless_validation_using_string_false
|
||||
# When the evaluated string returns false
|
||||
Topic.validates_length_of( :title, :maximum=>5, :too_long=>"hoo %d", :unless => "false")
|
||||
Topic.validates_length_of( :title, :maximum=>5, :too_long=>"hoo {{count}}", :unless => "false")
|
||||
t = Topic.create("title" => "uhohuhoh", "content" => "whatever")
|
||||
assert !t.valid?
|
||||
assert t.errors.on(:title)
|
||||
|
@ -1291,7 +1309,7 @@ class ValidationsTest < ActiveRecord::TestCase
|
|||
|
||||
def test_if_validation_using_block_true
|
||||
# When the block returns true
|
||||
Topic.validates_length_of( :title, :maximum=>5, :too_long=>"hoo %d",
|
||||
Topic.validates_length_of( :title, :maximum=>5, :too_long=>"hoo {{count}}",
|
||||
:if => Proc.new { |r| r.content.size > 4 } )
|
||||
t = Topic.create("title" => "uhohuhoh", "content" => "whatever")
|
||||
assert !t.valid?
|
||||
|
@ -1301,7 +1319,7 @@ class ValidationsTest < ActiveRecord::TestCase
|
|||
|
||||
def test_unless_validation_using_block_true
|
||||
# When the block returns true
|
||||
Topic.validates_length_of( :title, :maximum=>5, :too_long=>"hoo %d",
|
||||
Topic.validates_length_of( :title, :maximum=>5, :too_long=>"hoo {{count}}",
|
||||
:unless => Proc.new { |r| r.content.size > 4 } )
|
||||
t = Topic.create("title" => "uhohuhoh", "content" => "whatever")
|
||||
assert t.valid?
|
||||
|
@ -1310,7 +1328,7 @@ class ValidationsTest < ActiveRecord::TestCase
|
|||
|
||||
def test_if_validation_using_block_false
|
||||
# When the block returns false
|
||||
Topic.validates_length_of( :title, :maximum=>5, :too_long=>"hoo %d",
|
||||
Topic.validates_length_of( :title, :maximum=>5, :too_long=>"hoo {{count}}",
|
||||
:if => Proc.new { |r| r.title != "uhohuhoh"} )
|
||||
t = Topic.create("title" => "uhohuhoh", "content" => "whatever")
|
||||
assert t.valid?
|
||||
|
@ -1319,7 +1337,7 @@ class ValidationsTest < ActiveRecord::TestCase
|
|||
|
||||
def test_unless_validation_using_block_false
|
||||
# When the block returns false
|
||||
Topic.validates_length_of( :title, :maximum=>5, :too_long=>"hoo %d",
|
||||
Topic.validates_length_of( :title, :maximum=>5, :too_long=>"hoo {{count}}",
|
||||
:unless => Proc.new { |r| r.title != "uhohuhoh"} )
|
||||
t = Topic.create("title" => "uhohuhoh", "content" => "whatever")
|
||||
assert !t.valid?
|
||||
|
@ -1421,8 +1439,8 @@ class ValidatesNumericalityTest < ActiveRecord::TestCase
|
|||
def test_validates_numericality_of_with_nil_allowed
|
||||
Topic.validates_numericality_of :approved, :allow_nil => true
|
||||
|
||||
invalid!(BLANK + JUNK)
|
||||
valid!(NIL + FLOATS + INTEGERS + BIGDECIMAL + INFINITY)
|
||||
invalid!(JUNK)
|
||||
valid!(NIL + BLANK + FLOATS + INTEGERS + BIGDECIMAL + INFINITY)
|
||||
end
|
||||
|
||||
def test_validates_numericality_of_with_integer_only
|
||||
|
@ -1435,8 +1453,8 @@ class ValidatesNumericalityTest < ActiveRecord::TestCase
|
|||
def test_validates_numericality_of_with_integer_only_and_nil_allowed
|
||||
Topic.validates_numericality_of :approved, :only_integer => true, :allow_nil => true
|
||||
|
||||
invalid!(BLANK + JUNK + FLOATS + BIGDECIMAL + INFINITY)
|
||||
valid!(NIL + INTEGERS)
|
||||
invalid!(JUNK + FLOATS + BIGDECIMAL + INFINITY)
|
||||
valid!(NIL + BLANK + INTEGERS)
|
||||
end
|
||||
|
||||
def test_validates_numericality_with_greater_than
|
||||
|
@ -1496,13 +1514,13 @@ class ValidatesNumericalityTest < ActiveRecord::TestCase
|
|||
end
|
||||
|
||||
def test_validates_numericality_with_numeric_message
|
||||
Topic.validates_numericality_of :approved, :less_than => 4, :message => "smaller than %d"
|
||||
Topic.validates_numericality_of :approved, :less_than => 4, :message => "smaller than {{count}}"
|
||||
topic = Topic.new("title" => "numeric test", "approved" => 10)
|
||||
|
||||
assert !topic.valid?
|
||||
assert_equal "smaller than 4", topic.errors.on(:approved)
|
||||
|
||||
Topic.validates_numericality_of :approved, :greater_than => 4, :message => "greater than %d"
|
||||
Topic.validates_numericality_of :approved, :greater_than => 4, :message => "greater than {{count}}"
|
||||
topic = Topic.new("title" => "numeric test", "approved" => 1)
|
||||
|
||||
assert !topic.valid?
|
||||
|
|
|
@ -2,9 +2,7 @@ print "Using native MySQL\n"
|
|||
require_dependency 'models/course'
|
||||
require 'logger'
|
||||
|
||||
RAILS_DEFAULT_LOGGER = Logger.new('debug.log')
|
||||
RAILS_DEFAULT_LOGGER.level = Logger::DEBUG
|
||||
ActiveRecord::Base.logger = RAILS_DEFAULT_LOGGER
|
||||
ActiveRecord::Base.logger = Logger.new("debug.log")
|
||||
|
||||
# GRANT ALL PRIVILEGES ON activerecord_unittest.* to 'rails'@'localhost';
|
||||
# GRANT ALL PRIVILEGES ON activerecord_unittest2.* to 'rails'@'localhost';
|
||||
|
|
1
vendor/rails/activerecord/test/fixtures/.gitignore
vendored
Normal file
1
vendor/rails/activerecord/test/fixtures/.gitignore
vendored
Normal file
|
@ -0,0 +1 @@
|
|||
*.sqlite*
|
|
@ -5,6 +5,7 @@ first_client:
|
|||
client_of: 2
|
||||
name: Summit
|
||||
ruby_type: Client
|
||||
firm_name: 37signals
|
||||
|
||||
first_firm:
|
||||
id: 1
|
||||
|
|
|
@ -6,7 +6,7 @@ david:
|
|||
address_city: Scary Town
|
||||
address_country: Loony Land
|
||||
gps_location: 35.544623640962634x-105.9309951055148
|
||||
|
||||
|
||||
zaphod:
|
||||
id: 2
|
||||
name: Zaphod
|
||||
|
@ -14,4 +14,13 @@ zaphod:
|
|||
address_street: Avenue Road
|
||||
address_city: Hamlet Town
|
||||
address_country: Nation Land
|
||||
gps_location: NULL
|
||||
|
||||
barney:
|
||||
id: 3
|
||||
name: Barney Gumble
|
||||
balance: 1
|
||||
address_street: Quiet Road
|
||||
address_city: Peaceful Town
|
||||
address_country: Tranquil Land
|
||||
gps_location: NULL
|
10
vendor/rails/activerecord/test/migrations/broken/100_migration_that_raises_exception.rb
vendored
Normal file
10
vendor/rails/activerecord/test/migrations/broken/100_migration_that_raises_exception.rb
vendored
Normal file
|
@ -0,0 +1,10 @@
|
|||
class MigrationThatRaisesException < ActiveRecord::Migration
|
||||
def self.up
|
||||
add_column "people", "last_name", :string
|
||||
raise 'Something broke'
|
||||
end
|
||||
|
||||
def self.down
|
||||
remove_column "people", "last_name"
|
||||
end
|
||||
end
|
|
@ -2,6 +2,7 @@ class Author < ActiveRecord::Base
|
|||
has_many :posts
|
||||
has_many :posts_with_comments, :include => :comments, :class_name => "Post"
|
||||
has_many :posts_with_comments_sorted_by_comment_id, :include => :comments, :class_name => "Post", :order => 'comments.id'
|
||||
has_many :posts_sorted_by_id_limited, :class_name => "Post", :order => 'posts.id', :limit => 1
|
||||
has_many :posts_with_categories, :include => :categories, :class_name => "Post"
|
||||
has_many :posts_with_comments_and_categories, :include => [ :comments, :categories ], :order => "posts.id", :class_name => "Post"
|
||||
has_many :posts_containing_the_letter_a, :class_name => "Post"
|
||||
|
@ -16,6 +17,8 @@ class Author < ActiveRecord::Base
|
|||
proxy_target
|
||||
end
|
||||
end
|
||||
has_one :post_about_thinking, :class_name => 'Post', :conditions => "posts.title like '%thinking%'"
|
||||
has_one :post_about_thinking_with_last_comment, :class_name => 'Post', :conditions => "posts.title like '%thinking%'", :include => :last_comment
|
||||
has_many :comments, :through => :posts
|
||||
has_many :comments_containing_the_letter_e, :through => :posts, :source => :comments
|
||||
has_many :comments_with_order_and_conditions, :through => :posts, :source => :comments, :order => 'comments.body', :conditions => "comments.body like 'Thank%'"
|
||||
|
|
|
@ -13,6 +13,9 @@ class Category < ActiveRecord::Base
|
|||
has_and_belongs_to_many :post_with_conditions,
|
||||
:class_name => 'Post',
|
||||
:conditions => { :title => 'Yet Another Testing Title' }
|
||||
|
||||
has_and_belongs_to_many :posts_gruoped_by_title, :class_name => "Post", :group => "title", :select => "title"
|
||||
|
||||
def self.what_are_you
|
||||
'a category...'
|
||||
end
|
||||
|
|
|
@ -4,4 +4,10 @@ class Club < ActiveRecord::Base
|
|||
has_many :current_memberships
|
||||
has_one :sponsor
|
||||
has_one :sponsored_member, :through => :sponsor, :source => :sponsorable, :source_type => "Member"
|
||||
|
||||
private
|
||||
|
||||
def private_method
|
||||
"I'm sorry sir, this is a *private* club, not a *pirate* club"
|
||||
end
|
||||
end
|
26
vendor/rails/activerecord/test/models/company.rb
vendored
26
vendor/rails/activerecord/test/models/company.rb
vendored
|
@ -13,6 +13,12 @@ class Company < AbstractCompany
|
|||
def arbitrary_method
|
||||
"I am Jack's profound disappointment"
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
def private_method
|
||||
"I am Jack's innermost fears and aspirations"
|
||||
end
|
||||
end
|
||||
|
||||
module Namespaced
|
||||
|
@ -53,11 +59,16 @@ class Firm < Company
|
|||
has_many :clients_using_finder_sql, :class_name => "Client", :finder_sql => 'SELECT * FROM companies WHERE 1=1'
|
||||
has_many :plain_clients, :class_name => 'Client'
|
||||
has_many :readonly_clients, :class_name => 'Client', :readonly => true
|
||||
has_many :clients_using_primary_key, :class_name => 'Client',
|
||||
:primary_key => 'name', :foreign_key => 'firm_name'
|
||||
has_many :clients_grouped_by_firm_id, :class_name => "Client", :group => "firm_id", :select => "firm_id"
|
||||
has_many :clients_grouped_by_name, :class_name => "Client", :group => "name", :select => "name"
|
||||
|
||||
has_one :account, :foreign_key => "firm_id", :dependent => :destroy, :validate => true
|
||||
has_one :unvalidated_account, :foreign_key => "firm_id", :class_name => 'Account', :validate => false
|
||||
has_one :account_with_select, :foreign_key => "firm_id", :select => "id, firm_id", :class_name=>'Account'
|
||||
has_one :readonly_account, :foreign_key => "firm_id", :class_name => "Account", :readonly => true
|
||||
has_one :account_using_primary_key, :primary_key => "firm_id", :class_name => "Account"
|
||||
end
|
||||
|
||||
class DependentFirm < Company
|
||||
|
@ -101,6 +112,14 @@ class Client < Company
|
|||
def rating?
|
||||
query_attribute :rating
|
||||
end
|
||||
|
||||
class << self
|
||||
private
|
||||
|
||||
def private_method
|
||||
"darkness"
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
|
||||
|
@ -124,9 +143,14 @@ class Account < ActiveRecord::Base
|
|||
true
|
||||
end
|
||||
|
||||
|
||||
protected
|
||||
def validate
|
||||
errors.add_on_empty "credit_limit"
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
def private_method
|
||||
"Sir, yes sir!"
|
||||
end
|
||||
end
|
||||
|
|
|
@ -1,7 +1,8 @@
|
|||
class Customer < ActiveRecord::Base
|
||||
composed_of :address, :mapping => [ %w(address_street street), %w(address_city city), %w(address_country country) ], :allow_nil => true
|
||||
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 :gps_location, :allow_nil => true
|
||||
composed_of :fullname, :mapping => %w(name to_s), :constructor => Proc.new { |name| Fullname.parse(name) }, :converter => :parse
|
||||
end
|
||||
|
||||
class Address
|
||||
|
@ -53,3 +54,20 @@ class GpsLocation
|
|||
self.latitude == other.latitude && self.longitude == other.longitude
|
||||
end
|
||||
end
|
||||
|
||||
class Fullname
|
||||
attr_reader :first, :last
|
||||
|
||||
def self.parse(str)
|
||||
return nil unless str
|
||||
new(*str.to_s.split)
|
||||
end
|
||||
|
||||
def initialize(first, last = nil)
|
||||
@first, @last = first, last
|
||||
end
|
||||
|
||||
def to_s
|
||||
"#{first} #{last.upcase}"
|
||||
end
|
||||
end
|
|
@ -3,6 +3,7 @@ class Parrot < ActiveRecord::Base
|
|||
has_and_belongs_to_many :pirates
|
||||
has_and_belongs_to_many :treasures
|
||||
has_many :loots, :as => :looter
|
||||
alias_attribute :title, :name
|
||||
end
|
||||
|
||||
class LiveParrot < Parrot
|
||||
|
|
|
@ -13,6 +13,7 @@ class Post < ActiveRecord::Base
|
|||
end
|
||||
|
||||
belongs_to :author_with_posts, :class_name => "Author", :foreign_key => :author_id, :include => :posts
|
||||
belongs_to :author_with_address, :class_name => "Author", :foreign_key => :author_id, :include => :author_address
|
||||
|
||||
has_one :last_comment, :class_name => 'Comment', :order => 'id desc'
|
||||
|
||||
|
@ -22,6 +23,8 @@ class Post < ActiveRecord::Base
|
|||
end
|
||||
end
|
||||
|
||||
has_many :author_favorites, :through => :author
|
||||
|
||||
has_many :comments_with_interpolated_conditions, :class_name => 'Comment',
|
||||
:conditions => ['#{"#{aliased_table_name}." rescue ""}body = ?', 'Thank you for the welcome']
|
||||
|
||||
|
|
|
@ -4,6 +4,9 @@ class Topic < ActiveRecord::Base
|
|||
{ :conditions => ['written_on < ?', time] }
|
||||
}
|
||||
named_scope :approved, :conditions => {:approved => true}
|
||||
named_scope :by_lifo, :conditions => {:author_name => 'lifo'}
|
||||
|
||||
named_scope :approved_as_hash_condition, :conditions => {:topics => {:approved => true}}
|
||||
named_scope 'approved_as_string', :conditions => {:approved => true}
|
||||
named_scope :replied, :conditions => ['replies_count > 0']
|
||||
named_scope :anonymous_extension do
|
||||
|
|
|
@ -60,7 +60,7 @@ ActiveRecord::Schema.define do
|
|||
end
|
||||
|
||||
create_table :booleantests, :force => true do |t|
|
||||
t.integer :value
|
||||
t.boolean :value
|
||||
end
|
||||
|
||||
create_table :categories, :force => true do |t|
|
||||
|
@ -103,6 +103,7 @@ ActiveRecord::Schema.define do
|
|||
t.string :type
|
||||
t.string :ruby_type
|
||||
t.integer :firm_id
|
||||
t.string :firm_name
|
||||
t.string :name
|
||||
t.integer :client_of
|
||||
t.integer :rating, :default => 1
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue