Update to Rails 2.3.8
This commit is contained in:
parent
6677b46cb4
commit
f0635301aa
429 changed files with 17683 additions and 4047 deletions
43
vendor/rails/activerecord/CHANGELOG
vendored
43
vendor/rails/activerecord/CHANGELOG
vendored
|
@ -1,3 +1,45 @@
|
|||
*2.3.8 (May 24, 2010)*
|
||||
|
||||
* Version bump.
|
||||
|
||||
|
||||
*2.3.7 (May 24, 2010)*
|
||||
|
||||
* Version bump.
|
||||
|
||||
|
||||
*2.3.6 (May 23, 2010)*
|
||||
|
||||
* Add index length support for MySQL. #1852 [Emili Parreno, Pratik Naik]
|
||||
|
||||
Example:
|
||||
|
||||
add_index(:accounts, :name, :name => 'by_name', :length => 10)
|
||||
=> CREATE INDEX by_name ON accounts(name(10))
|
||||
|
||||
add_index(:accounts, [:name, :surname], :name => 'by_name_surname', :length => {:name => 10, :surname => 15})
|
||||
=> CREATE INDEX by_name_surname ON accounts(name(10), surname(15))
|
||||
|
||||
* find_or_create_by_attr(value, ...) works when attr is protected. #4457 [Santiago Pastorino, Marc-André Lafortune]
|
||||
|
||||
* JSON supports a custom root option: to_json(:root => 'custom') #4515 [Jatinder Singh]
|
||||
|
||||
* Destroy uses optimistic locking. If lock_version on the record you're destroying doesn't match lock_version in the database, a StaleObjectError is raised. #1966 [Curtis Hawthorne]
|
||||
|
||||
* To prefix the table names of all models in a module, define self.table_name_prefix on the module. #4032 [Andrew White]
|
||||
|
||||
* Association inverses for belongs_to, has_one, and has_many. Optimization to reduce database queries. #3533 [Murray Steele]
|
||||
|
||||
# post.comments sets each comment's post without needing to :include
|
||||
class Post < ActiveRecord::Base
|
||||
has_many :comments, :inverse_of => :post
|
||||
end
|
||||
|
||||
* MySQL: add_ and change_column support positioning. #3286 [Ben Marini]
|
||||
|
||||
* Reset your Active Record counter caches with the reset_counter_cache class method. #1211 [Mike Breen, Gabe da Silveira]
|
||||
|
||||
|
||||
*2.3.5 (November 25, 2009)*
|
||||
|
||||
* Minor Bug Fixes and deprecation warnings
|
||||
|
@ -6,6 +48,7 @@
|
|||
|
||||
* Numerous fixes to the nested attributes functionality
|
||||
|
||||
|
||||
*2.3.4 (September 4, 2009)*
|
||||
|
||||
* PostgreSQL: XML datatype support. #1874 [Leonardo Borges]
|
||||
|
|
2
vendor/rails/activerecord/MIT-LICENSE
vendored
2
vendor/rails/activerecord/MIT-LICENSE
vendored
|
@ -1,4 +1,4 @@
|
|||
Copyright (c) 2004-2009 David Heinemeier Hansson
|
||||
Copyright (c) 2004-2010 David Heinemeier Hansson
|
||||
|
||||
Permission is hereby granted, free of charge, to any person obtaining
|
||||
a copy of this software and associated documentation files (the
|
||||
|
|
2
vendor/rails/activerecord/Rakefile
vendored
2
vendor/rails/activerecord/Rakefile
vendored
|
@ -192,7 +192,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.3.5' + PKG_BUILD)
|
||||
s.add_dependency('activesupport', '= 2.3.8' + PKG_BUILD)
|
||||
|
||||
s.files.delete FIXTURES_ROOT + "/fixture_database.sqlite"
|
||||
s.files.delete FIXTURES_ROOT + "/fixture_database_2.sqlite"
|
||||
|
|
|
@ -1,5 +1,5 @@
|
|||
#--
|
||||
# Copyright (c) 2004-2009 David Heinemeier Hansson
|
||||
# Copyright (c) 2004-2010 David Heinemeier Hansson
|
||||
#
|
||||
# Permission is hereby granted, free of charge, to any person obtaining
|
||||
# a copy of this software and associated documentation files (the
|
||||
|
@ -80,5 +80,4 @@ module ActiveRecord
|
|||
end
|
||||
end
|
||||
|
||||
require 'active_record/i18n_interpolation_deprecation'
|
||||
I18n.load_path << File.dirname(__FILE__) + '/active_record/locale/en.yml'
|
||||
|
|
|
@ -126,6 +126,7 @@ module ActiveRecord
|
|||
association_proxy = parent_record.send(reflection_name)
|
||||
association_proxy.loaded
|
||||
association_proxy.target.push(*[associated_record].flatten)
|
||||
association_proxy.__send__(:set_inverse_instance, associated_record, parent_record)
|
||||
end
|
||||
end
|
||||
|
||||
|
@ -152,9 +153,15 @@ module ActiveRecord
|
|||
seen_keys[associated_record[key].to_s] = true
|
||||
mapped_records = id_to_record_map[associated_record[key].to_s]
|
||||
mapped_records.each do |mapped_record|
|
||||
mapped_record.send("set_#{reflection_name}_target", associated_record)
|
||||
association_proxy = mapped_record.send("set_#{reflection_name}_target", associated_record)
|
||||
association_proxy.__send__(:set_inverse_instance, associated_record, mapped_record)
|
||||
end
|
||||
end
|
||||
|
||||
id_to_record_map.each do |id, records|
|
||||
next if seen_keys.include?(id.to_s)
|
||||
records.each {|record| record.send("set_#{reflection_name}_target", nil) }
|
||||
end
|
||||
end
|
||||
|
||||
# Given a collection of ActiveRecord objects, constructs a Hash which maps
|
||||
|
@ -321,7 +328,7 @@ module ActiveRecord
|
|||
klass = klass_name.constantize
|
||||
|
||||
table_name = klass.quoted_table_name
|
||||
primary_key = klass.primary_key
|
||||
primary_key = reflection.options[:primary_key] || klass.primary_key
|
||||
column_type = klass.columns.detect{|c| c.name == primary_key}.type
|
||||
ids = id_map.keys.map do |id|
|
||||
if column_type == :integer
|
||||
|
|
|
@ -1,4 +1,10 @@
|
|||
module ActiveRecord
|
||||
class InverseOfAssociationNotFoundError < ActiveRecordError #:nodoc:
|
||||
def initialize(reflection, associated_class = nil)
|
||||
super("Could not find the inverse association for #{reflection.name} (#{reflection.options[:inverse_of].inspect} in #{associated_class.nil? ? reflection.class_name : associated_class.name})")
|
||||
end
|
||||
end
|
||||
|
||||
class HasManyThroughAssociationNotFoundError < ActiveRecordError #:nodoc:
|
||||
def initialize(owner_class_name, reflection)
|
||||
super("Could not find the association #{reflection.options[:through].inspect} in model #{owner_class_name}")
|
||||
|
@ -1247,7 +1253,7 @@ module ActiveRecord
|
|||
|
||||
if association.nil? || force_reload
|
||||
association = association_proxy_class.new(self, reflection)
|
||||
retval = association.reload
|
||||
retval = force_reload ? reflection.klass.uncached { association.reload } : association.reload
|
||||
if retval.nil? and association_proxy_class == BelongsToAssociation
|
||||
association_instance_set(reflection.name, nil)
|
||||
return nil
|
||||
|
@ -1301,7 +1307,7 @@ module ActiveRecord
|
|||
association_instance_set(reflection.name, association)
|
||||
end
|
||||
|
||||
association.reload if force_reload
|
||||
reflection.klass.uncached { association.reload } if force_reload
|
||||
|
||||
association
|
||||
end
|
||||
|
@ -1409,40 +1415,39 @@ module ActiveRecord
|
|||
# finder conditions.
|
||||
def configure_dependency_for_has_many(reflection, extra_conditions = nil)
|
||||
if reflection.options.include?(:dependent)
|
||||
# Add polymorphic type if the :as option is present
|
||||
dependent_conditions = []
|
||||
dependent_conditions << "#{reflection.primary_key_name} = \#{record.#{reflection.name}.send(:owner_quoted_id)}"
|
||||
dependent_conditions << "#{reflection.options[:as]}_type = '#{base_class.name}'" if reflection.options[:as]
|
||||
dependent_conditions << sanitize_sql(reflection.options[:conditions], reflection.quoted_table_name) if reflection.options[:conditions]
|
||||
dependent_conditions << extra_conditions if extra_conditions
|
||||
dependent_conditions = dependent_conditions.collect {|where| "(#{where})" }.join(" AND ")
|
||||
dependent_conditions = dependent_conditions.gsub('@', '\@')
|
||||
case reflection.options[:dependent]
|
||||
when :destroy
|
||||
method_name = "has_many_dependent_destroy_for_#{reflection.name}".to_sym
|
||||
define_method(method_name) do
|
||||
send(reflection.name).each { |o| o.destroy }
|
||||
send(reflection.name).each do |o|
|
||||
# No point in executing the counter update since we're going to destroy the parent anyway
|
||||
counter_method = ('belongs_to_counter_cache_before_destroy_for_' + self.class.name.downcase).to_sym
|
||||
if(o.respond_to? counter_method) then
|
||||
class << o
|
||||
self
|
||||
end.send(:define_method, counter_method, Proc.new {})
|
||||
end
|
||||
o.destroy
|
||||
end
|
||||
end
|
||||
before_destroy method_name
|
||||
when :delete_all
|
||||
module_eval %Q{
|
||||
before_destroy do |record| # before_destroy do |record|
|
||||
delete_all_has_many_dependencies(record, # delete_all_has_many_dependencies(record,
|
||||
"#{reflection.name}", # "posts",
|
||||
#{reflection.class_name}, # Post,
|
||||
%@#{dependent_conditions}@) # %@...@) # this is a string literal like %(...)
|
||||
end # end
|
||||
}
|
||||
before_destroy do |record|
|
||||
record.class.send(:delete_all_has_many_dependencies,
|
||||
record,
|
||||
reflection.name,
|
||||
reflection.klass,
|
||||
reflection.dependent_conditions(record, record.class, extra_conditions))
|
||||
end
|
||||
when :nullify
|
||||
module_eval %Q{
|
||||
before_destroy do |record| # before_destroy do |record|
|
||||
nullify_has_many_dependencies(record, # nullify_has_many_dependencies(record,
|
||||
"#{reflection.name}", # "posts",
|
||||
#{reflection.class_name}, # Post,
|
||||
"#{reflection.primary_key_name}", # "user_id",
|
||||
%@#{dependent_conditions}@) # %@...@) # this is a string literal like %(...)
|
||||
end # end
|
||||
}
|
||||
before_destroy do |record|
|
||||
record.class.send(:nullify_has_many_dependencies,
|
||||
record,
|
||||
reflection.name,
|
||||
reflection.klass,
|
||||
reflection.primary_key_name,
|
||||
reflection.dependent_conditions(record, record.class, extra_conditions))
|
||||
end
|
||||
else
|
||||
raise ArgumentError, "The :dependent option expects either :destroy, :delete_all, or :nullify (#{reflection.options[:dependent].inspect})"
|
||||
end
|
||||
|
@ -1526,7 +1531,7 @@ module ActiveRecord
|
|||
:finder_sql, :counter_sql,
|
||||
:before_add, :after_add, :before_remove, :after_remove,
|
||||
:extend, :readonly,
|
||||
:validate
|
||||
:validate, :inverse_of
|
||||
]
|
||||
|
||||
def create_has_many_reflection(association_id, options, &extension)
|
||||
|
@ -1540,7 +1545,7 @@ module ActiveRecord
|
|||
@@valid_keys_for_has_one_association = [
|
||||
:class_name, :foreign_key, :remote, :select, :conditions, :order,
|
||||
:include, :dependent, :counter_cache, :extend, :as, :readonly,
|
||||
:validate, :primary_key
|
||||
:validate, :primary_key, :inverse_of
|
||||
]
|
||||
|
||||
def create_has_one_reflection(association_id, options)
|
||||
|
@ -1559,7 +1564,7 @@ module ActiveRecord
|
|||
@@valid_keys_for_belongs_to_association = [
|
||||
:class_name, :primary_key, :foreign_key, :foreign_type, :remote, :select, :conditions,
|
||||
:include, :dependent, :counter_cache, :extend, :polymorphic, :readonly,
|
||||
:validate, :touch
|
||||
:validate, :touch, :inverse_of
|
||||
]
|
||||
|
||||
def create_belongs_to_reflection(association_id, options)
|
||||
|
@ -1777,7 +1782,7 @@ module ActiveRecord
|
|||
end
|
||||
|
||||
def using_limitable_reflections?(reflections)
|
||||
reflections.reject { |r| [ :belongs_to, :has_one ].include?(r.macro) }.length.zero?
|
||||
reflections.collect(&:collection?).length.zero?
|
||||
end
|
||||
|
||||
def column_aliases(join_dependency)
|
||||
|
@ -1854,7 +1859,7 @@ module ActiveRecord
|
|||
case associations
|
||||
when Symbol, String
|
||||
reflection = base.reflections[associations]
|
||||
if reflection && [:has_many, :has_and_belongs_to_many].include?(reflection.macro)
|
||||
if reflection && reflection.collection?
|
||||
records.each { |record| record.send(reflection.name).target.uniq! }
|
||||
end
|
||||
when Array
|
||||
|
@ -1864,12 +1869,11 @@ module ActiveRecord
|
|||
when Hash
|
||||
associations.keys.each do |name|
|
||||
reflection = base.reflections[name]
|
||||
is_collection = [:has_many, :has_and_belongs_to_many].include?(reflection.macro)
|
||||
|
||||
parent_records = records.map do |record|
|
||||
descendant = record.send(reflection.name)
|
||||
next unless descendant
|
||||
descendant.target.uniq! if is_collection
|
||||
descendant.target.uniq! if reflection.collection?
|
||||
descendant
|
||||
end.flatten.compact
|
||||
|
||||
|
@ -1960,21 +1964,27 @@ module ActiveRecord
|
|||
return nil if record.id.to_s != join.parent.record_id(row).to_s or row[join.aliased_primary_key].nil?
|
||||
association = join.instantiate(row)
|
||||
collection.target.push(association)
|
||||
collection.__send__(:set_inverse_instance, association, record)
|
||||
when :has_one
|
||||
return if record.id.to_s != join.parent.record_id(row).to_s
|
||||
return if record.instance_variable_defined?("@#{join.reflection.name}")
|
||||
association = join.instantiate(row) unless row[join.aliased_primary_key].nil?
|
||||
record.send("set_#{join.reflection.name}_target", association)
|
||||
set_target_and_inverse(join, association, record)
|
||||
when :belongs_to
|
||||
return if record.id.to_s != join.parent.record_id(row).to_s or row[join.aliased_primary_key].nil?
|
||||
association = join.instantiate(row)
|
||||
record.send("set_#{join.reflection.name}_target", association)
|
||||
set_target_and_inverse(join, association, record)
|
||||
else
|
||||
raise ConfigurationError, "unknown macro: #{join.reflection.macro}"
|
||||
end
|
||||
return association
|
||||
end
|
||||
|
||||
def set_target_and_inverse(join, association, record)
|
||||
association_proxy = record.send("set_#{join.reflection.name}_target", association)
|
||||
association_proxy.__send__(:set_inverse_instance, association, record)
|
||||
end
|
||||
|
||||
class JoinBase # :nodoc:
|
||||
attr_reader :active_record, :table_joins
|
||||
delegate :table_name, :column_names, :primary_key, :reflections, :sanitize_sql, :to => :active_record
|
||||
|
@ -2162,7 +2172,7 @@ module ActiveRecord
|
|||
" #{join_type} %s ON %s.%s = %s.%s " % [
|
||||
table_name_and_alias,
|
||||
connection.quote_table_name(aliased_table_name),
|
||||
reflection.klass.primary_key,
|
||||
reflection.options[:primary_key] || reflection.klass.primary_key,
|
||||
connection.quote_table_name(parent.aliased_table_name),
|
||||
options[:foreign_key] || reflection.primary_key_name
|
||||
]
|
||||
|
|
|
@ -400,11 +400,24 @@ module ActiveRecord
|
|||
find(:all)
|
||||
end
|
||||
|
||||
@reflection.options[:uniq] ? uniq(records) : records
|
||||
records = @reflection.options[:uniq] ? uniq(records) : records
|
||||
records.each do |record|
|
||||
set_inverse_instance(record, @owner)
|
||||
end
|
||||
records
|
||||
end
|
||||
|
||||
def add_record_to_target_with_callbacks(record)
|
||||
callback(:before_add, record)
|
||||
yield(record) if block_given?
|
||||
@target ||= [] unless loaded?
|
||||
@target << record unless @reflection.options[:uniq] && @target.include?(record)
|
||||
callback(:after_add, record)
|
||||
set_inverse_instance(record, @owner)
|
||||
record
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
def create_record(attrs)
|
||||
attrs.update(@reflection.options[:conditions]) if @reflection.options[:conditions].is_a?(Hash)
|
||||
ensure_owner_is_not_new
|
||||
|
@ -428,15 +441,6 @@ module ActiveRecord
|
|||
end
|
||||
end
|
||||
|
||||
def add_record_to_target_with_callbacks(record)
|
||||
callback(:before_add, record)
|
||||
yield(record) if block_given?
|
||||
@target ||= [] unless loaded?
|
||||
@target << record unless @reflection.options[:uniq] && @target.include?(record)
|
||||
callback(:after_add, record)
|
||||
record
|
||||
end
|
||||
|
||||
def remove_records(*records)
|
||||
records = flatten_deeper(records)
|
||||
records.each { |record| raise_on_type_mismatch(record) }
|
||||
|
|
|
@ -53,6 +53,7 @@ module ActiveRecord
|
|||
|
||||
def initialize(owner, reflection)
|
||||
@owner, @reflection = owner, reflection
|
||||
reflection.check_validity!
|
||||
Array(reflection.options[:extend]).each { |ext| proxy_extend(ext) }
|
||||
reset
|
||||
end
|
||||
|
@ -208,14 +209,10 @@ module ActiveRecord
|
|||
|
||||
private
|
||||
# Forwards any missing method call to the \target.
|
||||
def method_missing(method, *args)
|
||||
def method_missing(method, *args, &block)
|
||||
if load_target
|
||||
if @target.respond_to?(method)
|
||||
if block_given?
|
||||
@target.send(method, *args) { |*block_args| yield(*block_args) }
|
||||
else
|
||||
@target.send(method, *args)
|
||||
end
|
||||
@target.send(method, *args, &block)
|
||||
else
|
||||
super
|
||||
end
|
||||
|
@ -273,6 +270,19 @@ module ActiveRecord
|
|||
def owner_quoted_id
|
||||
@owner.quoted_id
|
||||
end
|
||||
|
||||
def set_inverse_instance(record, instance)
|
||||
return if record.nil? || !we_can_set_the_inverse_on_this?(record)
|
||||
inverse_relationship = @reflection.inverse_of
|
||||
unless inverse_relationship.nil?
|
||||
record.send(:"set_#{inverse_relationship.name}_target", instance)
|
||||
end
|
||||
end
|
||||
|
||||
# Override in subclasses
|
||||
def we_can_set_the_inverse_on_this?(record)
|
||||
false
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
|
|
@ -31,6 +31,8 @@ module ActiveRecord
|
|||
@updated = true
|
||||
end
|
||||
|
||||
set_inverse_instance(record, @owner)
|
||||
|
||||
loaded
|
||||
record
|
||||
end
|
||||
|
@ -46,13 +48,15 @@ module ActiveRecord
|
|||
else
|
||||
"find"
|
||||
end
|
||||
@reflection.klass.send(find_method,
|
||||
the_target = @reflection.klass.send(find_method,
|
||||
@owner[@reflection.primary_key_name],
|
||||
:select => @reflection.options[:select],
|
||||
:conditions => conditions,
|
||||
:include => @reflection.options[:include],
|
||||
:readonly => @reflection.options[:readonly]
|
||||
) if @owner[@reflection.primary_key_name]
|
||||
set_inverse_instance(the_target, @owner)
|
||||
the_target
|
||||
end
|
||||
|
||||
def foreign_key_present
|
||||
|
@ -71,6 +75,12 @@ module ActiveRecord
|
|||
@owner[@reflection.primary_key_name]
|
||||
end
|
||||
end
|
||||
|
||||
# NOTE - for now, we're only supporting inverse setting from belongs_to back onto
|
||||
# has_one associations.
|
||||
def we_can_set_the_inverse_on_this?(record)
|
||||
@reflection.has_inverse? && @reflection.inverse_of.macro == :has_one
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
|
|
@ -13,6 +13,7 @@ module ActiveRecord
|
|||
@updated = true
|
||||
end
|
||||
|
||||
set_inverse_instance(record, @owner)
|
||||
loaded
|
||||
record
|
||||
end
|
||||
|
@ -22,19 +23,42 @@ module ActiveRecord
|
|||
end
|
||||
|
||||
private
|
||||
|
||||
# NOTE - for now, we're only supporting inverse setting from belongs_to back onto
|
||||
# has_one associations.
|
||||
def we_can_set_the_inverse_on_this?(record)
|
||||
if @reflection.has_inverse?
|
||||
inverse_association = @reflection.polymorphic_inverse_of(record.class)
|
||||
inverse_association && inverse_association.macro == :has_one
|
||||
else
|
||||
false
|
||||
end
|
||||
end
|
||||
|
||||
def set_inverse_instance(record, instance)
|
||||
return if record.nil? || !we_can_set_the_inverse_on_this?(record)
|
||||
inverse_relationship = @reflection.polymorphic_inverse_of(record.class)
|
||||
unless inverse_relationship.nil?
|
||||
record.send(:"set_#{inverse_relationship.name}_target", instance)
|
||||
end
|
||||
end
|
||||
|
||||
def find_target
|
||||
return nil if association_class.nil?
|
||||
|
||||
if @reflection.options[:conditions]
|
||||
association_class.find(
|
||||
@owner[@reflection.primary_key_name],
|
||||
:select => @reflection.options[:select],
|
||||
:conditions => conditions,
|
||||
:include => @reflection.options[:include]
|
||||
)
|
||||
else
|
||||
association_class.find(@owner[@reflection.primary_key_name], :select => @reflection.options[:select], :include => @reflection.options[:include])
|
||||
end
|
||||
target =
|
||||
if @reflection.options[:conditions]
|
||||
association_class.find(
|
||||
@owner[@reflection.primary_key_name],
|
||||
:select => @reflection.options[:select],
|
||||
:conditions => conditions,
|
||||
:include => @reflection.options[:include]
|
||||
)
|
||||
else
|
||||
association_class.find(@owner[@reflection.primary_key_name], :select => @reflection.options[:select], :include => @reflection.options[:include])
|
||||
end
|
||||
set_inverse_instance(target, @owner)
|
||||
target
|
||||
end
|
||||
|
||||
def foreign_key_present
|
||||
|
|
|
@ -117,6 +117,11 @@ module ActiveRecord
|
|||
:create => create_scoping
|
||||
}
|
||||
end
|
||||
|
||||
def we_can_set_the_inverse_on_this?(record)
|
||||
inverse = @reflection.inverse_of
|
||||
return !inverse.nil?
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
|
|
@ -1,11 +1,6 @@
|
|||
module ActiveRecord
|
||||
module Associations
|
||||
class HasManyThroughAssociation < HasManyAssociation #:nodoc:
|
||||
def initialize(owner, reflection)
|
||||
reflection.check_validity!
|
||||
super
|
||||
end
|
||||
|
||||
alias_method :new, :build
|
||||
|
||||
def create!(attrs = nil)
|
||||
|
@ -261,6 +256,11 @@ module ActiveRecord
|
|||
def cached_counter_attribute_name
|
||||
"#{@reflection.name}_count"
|
||||
end
|
||||
|
||||
# NOTE - not sure that we can actually cope with inverses here
|
||||
def we_can_set_the_inverse_on_this?(record)
|
||||
false
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
|
|
@ -57,6 +57,7 @@ module ActiveRecord
|
|||
@target = (AssociationProxy === obj ? obj.target : obj)
|
||||
end
|
||||
|
||||
set_inverse_instance(obj, @owner)
|
||||
@loaded = true
|
||||
|
||||
unless @owner.new_record? or obj.nil? or dont_save
|
||||
|
@ -77,13 +78,15 @@ module ActiveRecord
|
|||
|
||||
private
|
||||
def find_target
|
||||
@reflection.klass.find(:first,
|
||||
the_target = @reflection.klass.find(:first,
|
||||
:conditions => @finder_sql,
|
||||
:select => @reflection.options[:select],
|
||||
:order => @reflection.options[:order],
|
||||
:include => @reflection.options[:include],
|
||||
:readonly => @reflection.options[:readonly]
|
||||
)
|
||||
set_inverse_instance(the_target, @owner)
|
||||
the_target
|
||||
end
|
||||
|
||||
def construct_sql
|
||||
|
@ -118,6 +121,7 @@ module ActiveRecord
|
|||
else
|
||||
record[@reflection.primary_key_name] = @owner.id unless @owner.new_record?
|
||||
self.target = record
|
||||
set_inverse_instance(record, @owner)
|
||||
end
|
||||
|
||||
record
|
||||
|
@ -128,6 +132,11 @@ module ActiveRecord
|
|||
attrs.update(@reflection.options[:conditions]) if @reflection.options[:conditions].is_a?(Hash)
|
||||
attrs
|
||||
end
|
||||
|
||||
def we_can_set_the_inverse_on_this?(record)
|
||||
inverse = @reflection.inverse_of
|
||||
return !inverse.nil?
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
|
|
@ -208,7 +208,7 @@ module ActiveRecord
|
|||
end
|
||||
|
||||
begin
|
||||
class_eval(method_definition, __FILE__, __LINE__)
|
||||
class_eval(method_definition, __FILE__)
|
||||
rescue SyntaxError => err
|
||||
generated_methods.delete(attr_name)
|
||||
if logger
|
||||
|
@ -230,6 +230,10 @@ module ActiveRecord
|
|||
# It's also possible to instantiate related objects, so a Client class belonging to the clients
|
||||
# table with a +master_id+ foreign key can instantiate master through Client#master.
|
||||
def method_missing(method_id, *args, &block)
|
||||
if method_id == :to_ary || method_id == :to_str
|
||||
raise NoMethodError, "undefined method `#{method_id}' for #{inspect}:#{self.class}"
|
||||
end
|
||||
|
||||
method_name = method_id.to_s
|
||||
|
||||
if self.class.private_method_defined?(method_name)
|
||||
|
|
|
@ -156,38 +156,42 @@ module ActiveRecord
|
|||
|
||||
# Adds a validate and save callback for the association as specified by
|
||||
# the +reflection+.
|
||||
#
|
||||
# For performance reasons, we don't check whether to validate at runtime,
|
||||
# but instead only define the method and callback when needed. However,
|
||||
# this can change, for instance, when using nested attributes, which is
|
||||
# called _after_ the association has been defined. Since we don't want
|
||||
# the callbacks to get defined multiple times, there are guards that
|
||||
# check if the save or validation methods have already been defined
|
||||
# before actually defining them.
|
||||
def add_autosave_association_callbacks(reflection)
|
||||
save_method = "autosave_associated_records_for_#{reflection.name}"
|
||||
validation_method = "validate_associated_records_for_#{reflection.name}"
|
||||
force_validation = (reflection.options[:validate] == true || reflection.options[:autosave] == true)
|
||||
save_method = :"autosave_associated_records_for_#{reflection.name}"
|
||||
validation_method = :"validate_associated_records_for_#{reflection.name}"
|
||||
collection = reflection.collection?
|
||||
|
||||
case reflection.macro
|
||||
when :has_many, :has_and_belongs_to_many
|
||||
before_save :before_save_collection_association
|
||||
unless method_defined?(save_method)
|
||||
if collection
|
||||
before_save :before_save_collection_association
|
||||
|
||||
define_method(save_method) { save_collection_association(reflection) }
|
||||
# Doesn't use after_save as that would save associations added in after_create/after_update twice
|
||||
after_create save_method
|
||||
after_update save_method
|
||||
|
||||
if force_validation || (reflection.macro == :has_many && reflection.options[:validate] != false)
|
||||
define_method(validation_method) { validate_collection_association(reflection) }
|
||||
validate validation_method
|
||||
end
|
||||
else
|
||||
case reflection.macro
|
||||
when :has_one
|
||||
define_method(save_method) { save_has_one_association(reflection) }
|
||||
after_save save_method
|
||||
when :belongs_to
|
||||
define_method(save_method) { save_belongs_to_association(reflection) }
|
||||
before_save save_method
|
||||
define_method(save_method) { save_collection_association(reflection) }
|
||||
# Doesn't use after_save as that would save associations added in after_create/after_update twice
|
||||
after_create save_method
|
||||
after_update save_method
|
||||
else
|
||||
if reflection.macro == :has_one
|
||||
define_method(save_method) { save_has_one_association(reflection) }
|
||||
after_save save_method
|
||||
else
|
||||
define_method(save_method) { save_belongs_to_association(reflection) }
|
||||
before_save save_method
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
if force_validation
|
||||
define_method(validation_method) { validate_single_association(reflection) }
|
||||
validate validation_method
|
||||
end
|
||||
if reflection.validate? && !method_defined?(validation_method)
|
||||
method = (collection ? :validate_collection_association : :validate_single_association)
|
||||
define_method(validation_method) { send(method, reflection) }
|
||||
validate validation_method
|
||||
end
|
||||
end
|
||||
end
|
||||
|
@ -213,6 +217,12 @@ module ActiveRecord
|
|||
@marked_for_destruction
|
||||
end
|
||||
|
||||
# Returns whether or not this record has been changed in any way (including whether
|
||||
# any of its nested autosave associations are likewise changed)
|
||||
def changed_for_autosave?
|
||||
new_record? || changed? || marked_for_destruction? || nested_records_changed_for_autosave?
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
# Returns the record for an association collection that should be validated
|
||||
|
@ -221,13 +231,28 @@ module ActiveRecord
|
|||
def associated_records_to_validate_or_save(association, new_record, autosave)
|
||||
if new_record
|
||||
association
|
||||
elsif association.loaded?
|
||||
autosave ? association : association.select { |record| record.new_record? }
|
||||
elsif autosave
|
||||
association.target.select { |record| record.changed_for_autosave? }
|
||||
else
|
||||
autosave ? association.target : association.target.select { |record| record.new_record? }
|
||||
association.target.select { |record| record.new_record? }
|
||||
end
|
||||
end
|
||||
|
||||
|
||||
# go through nested autosave associations that are loaded in memory (without loading
|
||||
# any new ones), and return true if is changed for autosave
|
||||
def nested_records_changed_for_autosave?
|
||||
self.class.reflect_on_all_autosave_associations.each do |reflection|
|
||||
if association = association_instance_get(reflection.name)
|
||||
if [:belongs_to, :has_one].include?(reflection.macro)
|
||||
return true if association.target && association.target.changed_for_autosave?
|
||||
else
|
||||
association.target.each {|record| return true if record.changed_for_autosave? }
|
||||
end
|
||||
end
|
||||
end
|
||||
false
|
||||
end
|
||||
|
||||
# Validate the association if <tt>:validate</tt> or <tt>:autosave</tt> is
|
||||
# turned on for the association specified by +reflection+.
|
||||
def validate_single_association(reflection)
|
||||
|
@ -293,13 +318,15 @@ module ActiveRecord
|
|||
association.destroy(record)
|
||||
elsif autosave != false && (@new_record_before_save || record.new_record?)
|
||||
if autosave
|
||||
association.send(:insert_record, record, false, false)
|
||||
saved = association.send(:insert_record, record, false, false)
|
||||
else
|
||||
association.send(:insert_record, record)
|
||||
end
|
||||
elsif autosave
|
||||
record.save(false)
|
||||
saved = record.save(false)
|
||||
end
|
||||
|
||||
raise ActiveRecord::Rollback if saved == false
|
||||
end
|
||||
end
|
||||
|
||||
|
@ -326,7 +353,9 @@ module ActiveRecord
|
|||
key = reflection.options[:primary_key] ? send(reflection.options[:primary_key]) : id
|
||||
if autosave != false && (new_record? || association.new_record? || association[reflection.primary_key_name] != key || autosave)
|
||||
association[reflection.primary_key_name] = key
|
||||
association.save(!autosave)
|
||||
saved = association.save(!autosave)
|
||||
raise ActiveRecord::Rollback if !saved && autosave
|
||||
saved
|
||||
end
|
||||
end
|
||||
end
|
||||
|
@ -347,7 +376,7 @@ module ActiveRecord
|
|||
if autosave && association.marked_for_destruction?
|
||||
association.destroy
|
||||
elsif autosave != false
|
||||
association.save(!autosave) if association.new_record? || autosave
|
||||
saved = association.save(!autosave) if association.new_record? || autosave
|
||||
|
||||
if association.updated?
|
||||
association_id = association.send(reflection.options[:primary_key] || :id)
|
||||
|
@ -357,6 +386,8 @@ module ActiveRecord
|
|||
self[reflection.options[:foreign_type]] = association.class.base_class.name.to_s
|
||||
end
|
||||
end
|
||||
|
||||
saved if autosave
|
||||
end
|
||||
end
|
||||
end
|
||||
|
|
113
vendor/rails/activerecord/lib/active_record/base.rb
vendored
113
vendor/rails/activerecord/lib/active_record/base.rb
vendored
|
@ -461,6 +461,9 @@ module ActiveRecord #:nodoc:
|
|||
# Accessor for the name of the prefix string to prepend to every table name. So if set to "basecamp_", all
|
||||
# table names will be named like "basecamp_projects", "basecamp_people", etc. This is a convenient way of creating a namespace
|
||||
# for tables in a shared database. By default, the prefix is the empty string.
|
||||
#
|
||||
# If you are organising your models within modules you can add a prefix to the models within a namespace by defining
|
||||
# a singleton method in the parent module called table_name_prefix which returns your chosen prefix.
|
||||
cattr_accessor :table_name_prefix, :instance_writer => false
|
||||
@@table_name_prefix = ""
|
||||
|
||||
|
@ -916,6 +919,29 @@ module ActiveRecord #:nodoc:
|
|||
connection.select_value(sql, "#{name} Count").to_i
|
||||
end
|
||||
|
||||
# Resets one or more counter caches to their correct value using an SQL
|
||||
# count query. This is useful when adding new counter caches, or if the
|
||||
# counter has been corrupted or modified directly by SQL.
|
||||
#
|
||||
# ==== Parameters
|
||||
#
|
||||
# * +id+ - The id of the object you wish to reset a counter on.
|
||||
# * +counters+ - One or more counter names to reset
|
||||
#
|
||||
# ==== Examples
|
||||
#
|
||||
# # For Post with id #1 records reset the comments_count
|
||||
# Post.reset_counters(1, :comments)
|
||||
def reset_counters(id, *counters)
|
||||
object = find(id)
|
||||
counters.each do |association|
|
||||
child_class = reflect_on_association(association).klass
|
||||
counter_name = child_class.reflect_on_association(self.name.downcase.to_sym).counter_cache_column
|
||||
|
||||
connection.update("UPDATE #{quoted_table_name} SET #{connection.quote_column_name(counter_name)} = #{object.send(association).count} WHERE #{connection.quote_column_name(primary_key)} = #{quote_value(object.id)}", "#{name} UPDATE")
|
||||
end
|
||||
end
|
||||
|
||||
# A generic "counter updater" implementation, intended primarily to be
|
||||
# used by increment_counter and decrement_counter, but which may also
|
||||
# be useful on its own. It simply does a direct SQL update for the record
|
||||
|
@ -1148,7 +1174,7 @@ module ActiveRecord #:nodoc:
|
|||
contained = contained.singularize if parent.pluralize_table_names
|
||||
contained << '_'
|
||||
end
|
||||
name = "#{table_name_prefix}#{contained}#{undecorated_table_name(base.name)}#{table_name_suffix}"
|
||||
name = "#{full_table_name_prefix}#{contained}#{undecorated_table_name(base.name)}#{table_name_suffix}"
|
||||
end
|
||||
|
||||
set_table_name(name)
|
||||
|
@ -1178,6 +1204,10 @@ module ActiveRecord #:nodoc:
|
|||
key
|
||||
end
|
||||
|
||||
def full_table_name_prefix #:nodoc:
|
||||
(parents.detect{ |p| p.respond_to?(:table_name_prefix) } || self).table_name_prefix
|
||||
end
|
||||
|
||||
# Defines the column name for use with single table inheritance
|
||||
# -- can be set in subclasses like so: self.inheritance_column = "type_id"
|
||||
def inheritance_column
|
||||
|
@ -1861,7 +1891,7 @@ module ActiveRecord #:nodoc:
|
|||
# find(:first, options.merge(finder_options))
|
||||
# end
|
||||
# end
|
||||
self.class_eval %{
|
||||
self.class_eval <<-EOS, __FILE__, __LINE__ + 1
|
||||
def self.#{method_id}(*args)
|
||||
options = args.extract_options!
|
||||
attributes = construct_attributes_from_arguments(
|
||||
|
@ -1881,7 +1911,7 @@ module ActiveRecord #:nodoc:
|
|||
end
|
||||
#{'result || raise(RecordNotFound, "Couldn\'t find #{name} with #{attributes.to_a.collect {|pair| "#{pair.first} = #{pair.second}"}.join(\', \')}")' if bang}
|
||||
end
|
||||
}, __FILE__, __LINE__
|
||||
EOS
|
||||
send(method_id, *arguments)
|
||||
elsif match.instantiator?
|
||||
instantiator = match.instantiator
|
||||
|
@ -1910,25 +1940,30 @@ module ActiveRecord #:nodoc:
|
|||
# record
|
||||
# end
|
||||
# end
|
||||
self.class_eval %{
|
||||
self.class_eval <<-EOS, __FILE__, __LINE__ + 1
|
||||
def self.#{method_id}(*args)
|
||||
guard_protected_attributes = false
|
||||
|
||||
if args[0].is_a?(Hash)
|
||||
guard_protected_attributes = true
|
||||
attributes = args[0].with_indifferent_access
|
||||
find_attributes = attributes.slice(*[:#{attribute_names.join(',:')}])
|
||||
else
|
||||
find_attributes = attributes = construct_attributes_from_arguments([:#{attribute_names.join(',:')}], args)
|
||||
attributes = [:#{attribute_names.join(',:')}]
|
||||
protected_attributes_for_create, unprotected_attributes_for_create = {}, {}
|
||||
args.each_with_index do |arg, i|
|
||||
if arg.is_a?(Hash)
|
||||
protected_attributes_for_create = args[i].with_indifferent_access
|
||||
else
|
||||
unprotected_attributes_for_create[attributes[i]] = args[i]
|
||||
end
|
||||
end
|
||||
|
||||
find_attributes = (protected_attributes_for_create.merge(unprotected_attributes_for_create)).slice(*attributes)
|
||||
|
||||
options = { :conditions => find_attributes }
|
||||
set_readonly_option!(options)
|
||||
|
||||
record = find(:first, options)
|
||||
|
||||
if record.nil?
|
||||
record = self.new { |r| r.send(:attributes=, attributes, guard_protected_attributes) }
|
||||
record = self.new do |r|
|
||||
r.send(:attributes=, protected_attributes_for_create, true) unless protected_attributes_for_create.empty?
|
||||
r.send(:attributes=, unprotected_attributes_for_create, false) unless unprotected_attributes_for_create.empty?
|
||||
end
|
||||
#{'yield(record) if block_given?'}
|
||||
#{'record.save' if instantiator == :create}
|
||||
record
|
||||
|
@ -1936,14 +1971,14 @@ module ActiveRecord #:nodoc:
|
|||
record
|
||||
end
|
||||
end
|
||||
}, __FILE__, __LINE__
|
||||
EOS
|
||||
send(method_id, *arguments, &block)
|
||||
end
|
||||
elsif match = DynamicScopeMatch.match(method_id)
|
||||
attribute_names = match.attribute_names
|
||||
super unless all_attributes_exists?(attribute_names)
|
||||
if match.scope?
|
||||
self.class_eval %{
|
||||
self.class_eval <<-EOS, __FILE__, __LINE__ + 1
|
||||
def self.#{method_id}(*args) # def self.scoped_by_user_name_and_password(*args)
|
||||
options = args.extract_options! # options = args.extract_options!
|
||||
attributes = construct_attributes_from_arguments( # attributes = construct_attributes_from_arguments(
|
||||
|
@ -1952,7 +1987,7 @@ module ActiveRecord #:nodoc:
|
|||
#
|
||||
scoped(:conditions => attributes) # scoped(:conditions => attributes)
|
||||
end # end
|
||||
}, __FILE__, __LINE__
|
||||
EOS
|
||||
send(method_id, *arguments)
|
||||
end
|
||||
else
|
||||
|
@ -2194,9 +2229,9 @@ module ActiveRecord #:nodoc:
|
|||
modularized_name = type_name_with_module(type_name)
|
||||
silence_warnings do
|
||||
begin
|
||||
class_eval(modularized_name, __FILE__, __LINE__)
|
||||
class_eval(modularized_name, __FILE__)
|
||||
rescue NameError
|
||||
class_eval(type_name, __FILE__, __LINE__)
|
||||
class_eval(type_name, __FILE__)
|
||||
end
|
||||
end
|
||||
end
|
||||
|
@ -2436,7 +2471,7 @@ module ActiveRecord #:nodoc:
|
|||
@new_record = true
|
||||
ensure_proper_type
|
||||
self.attributes = attributes unless attributes.nil?
|
||||
self.class.send(:scope, :create).each { |att,value| self.send("#{att}=", value) } if self.class.send(:scoped?, :create)
|
||||
assign_attributes(self.class.send(:scope, :create)) if self.class.send(:scoped?, :create)
|
||||
result = yield self if block_given?
|
||||
callback(:after_initialize) if respond_to_without_attributes?(:after_initialize)
|
||||
result
|
||||
|
@ -2693,7 +2728,7 @@ module ActiveRecord #:nodoc:
|
|||
def reload(options = nil)
|
||||
clear_aggregation_cache
|
||||
clear_association_cache
|
||||
@attributes.update(self.class.find(self.id, options).instance_variable_get('@attributes'))
|
||||
@attributes.update(self.class.send(:with_exclusive_scope) { self.class.find(self.id, options) }.instance_variable_get('@attributes'))
|
||||
@attributes_cache = {}
|
||||
self
|
||||
end
|
||||
|
@ -2736,26 +2771,15 @@ module ActiveRecord #:nodoc:
|
|||
attributes = new_attributes.dup
|
||||
attributes.stringify_keys!
|
||||
|
||||
multi_parameter_attributes = []
|
||||
attributes = remove_attributes_protected_from_mass_assignment(attributes) if guard_protected_attributes
|
||||
|
||||
attributes.each do |k, v|
|
||||
if k.include?("(")
|
||||
multi_parameter_attributes << [ k, v ]
|
||||
else
|
||||
respond_to?(:"#{k}=") ? send(:"#{k}=", v) : raise(UnknownAttributeError, "unknown attribute: #{k}")
|
||||
end
|
||||
end
|
||||
|
||||
assign_multiparameter_attributes(multi_parameter_attributes)
|
||||
assign_attributes(attributes) if attributes and attributes.any?
|
||||
end
|
||||
|
||||
# Returns a hash of all the attributes with their names as keys and the values of the attributes as values.
|
||||
def attributes
|
||||
self.attribute_names.inject({}) do |attrs, name|
|
||||
attrs[name] = read_attribute(name)
|
||||
attrs
|
||||
end
|
||||
attrs = {}
|
||||
attribute_names.each { |name| attrs[name] = read_attribute(name) }
|
||||
attrs
|
||||
end
|
||||
|
||||
# Returns a hash of attributes before typecasting and deserialization.
|
||||
|
@ -2869,6 +2893,23 @@ module ActiveRecord #:nodoc:
|
|||
end
|
||||
|
||||
private
|
||||
# Assigns attributes, dealing nicely with both multi and single paramater attributes
|
||||
# Assumes attributes is a hash
|
||||
|
||||
def assign_attributes(attributes={})
|
||||
multiparameter_attributes = []
|
||||
|
||||
attributes.each do |k, v|
|
||||
if k.to_s.include?("(")
|
||||
multiparameter_attributes << [ k, v ]
|
||||
else
|
||||
respond_to?(:"#{k}=") ? send(:"#{k}=", v) : raise(UnknownAttributeError, "unknown attribute: #{k}")
|
||||
end
|
||||
end
|
||||
|
||||
assign_multiparameter_attributes(multiparameter_attributes) unless multiparameter_attributes.empty?
|
||||
end
|
||||
|
||||
def create_or_update
|
||||
raise ReadOnlyRecord if readonly?
|
||||
result = new_record? ? create : update
|
||||
|
@ -3099,7 +3140,7 @@ module ActiveRecord #:nodoc:
|
|||
|
||||
# Returns a comma-separated pair list, like "key1 = val1, key2 = val2".
|
||||
def comma_pair_list(hash)
|
||||
hash.inject([]) { |list, pair| list << "#{pair.first} = #{pair.last}" }.join(", ")
|
||||
hash.map { |k,v| "#{k} = #{v}" }.join(", ")
|
||||
end
|
||||
|
||||
def quoted_column_names(attributes = attributes_with_quotes)
|
||||
|
|
|
@ -59,19 +59,23 @@ module ActiveRecord
|
|||
start = options.delete(:start).to_i
|
||||
batch_size = options.delete(:batch_size) || 1000
|
||||
|
||||
with_scope(:find => options.merge(:order => batch_order, :limit => batch_size)) do
|
||||
records = find(:all, :conditions => [ "#{table_name}.#{primary_key} >= ?", start ])
|
||||
proxy = scoped(options.merge(:order => batch_order, :limit => batch_size))
|
||||
records = proxy.find(:all, :conditions => [ "#{table_name}.#{primary_key} >= ?", start ])
|
||||
|
||||
while records.any?
|
||||
yield records
|
||||
while records.any?
|
||||
yield records
|
||||
|
||||
break if records.size < batch_size
|
||||
records = find(:all, :conditions => [ "#{table_name}.#{primary_key} > ?", records.last.id ])
|
||||
end
|
||||
break if records.size < batch_size
|
||||
|
||||
last_value = records.last.id
|
||||
|
||||
raise "You must include the primary key if you define a select" unless last_value.present?
|
||||
|
||||
records = proxy.find(:all, :conditions => [ "#{table_name}.#{primary_key} > ?", last_value ])
|
||||
end
|
||||
end
|
||||
|
||||
|
||||
|
||||
|
||||
private
|
||||
def batch_order
|
||||
"#{table_name}.#{primary_key} ASC"
|
||||
|
|
|
@ -294,12 +294,15 @@ module ActiveRecord
|
|||
end
|
||||
|
||||
def type_cast_calculated_value(value, column, operation = nil)
|
||||
operation = operation.to_s.downcase
|
||||
case operation
|
||||
if value.is_a?(String) || value.nil?
|
||||
case operation.to_s.downcase
|
||||
when 'count' then value.to_i
|
||||
when 'sum' then type_cast_using_column(value || '0', column)
|
||||
when 'avg' then value && (value.is_a?(Fixnum) ? value.to_f : value).to_d
|
||||
when 'avg' then value.try(:to_d)
|
||||
else type_cast_using_column(value, column)
|
||||
end
|
||||
else
|
||||
value
|
||||
end
|
||||
end
|
||||
|
||||
|
|
|
@ -10,8 +10,8 @@ module ActiveRecord
|
|||
##
|
||||
# :singleton-method:
|
||||
# The connection handler
|
||||
cattr_accessor :connection_handler, :instance_writer => false
|
||||
@@connection_handler = ConnectionAdapters::ConnectionHandler.new
|
||||
superclass_delegating_accessor :connection_handler
|
||||
self.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
|
||||
|
@ -54,7 +54,7 @@ module ActiveRecord
|
|||
raise AdapterNotSpecified unless defined? RAILS_ENV
|
||||
establish_connection(RAILS_ENV)
|
||||
when ConnectionSpecification
|
||||
@@connection_handler.establish_connection(name, spec)
|
||||
self.connection_handler.establish_connection(name, spec)
|
||||
when Symbol, String
|
||||
if configuration = configurations[spec.to_s]
|
||||
establish_connection(configuration)
|
||||
|
|
57
vendor/rails/activerecord/lib/active_record/connection_adapters/abstract/database_limits.rb
vendored
Normal file
57
vendor/rails/activerecord/lib/active_record/connection_adapters/abstract/database_limits.rb
vendored
Normal file
|
@ -0,0 +1,57 @@
|
|||
module ActiveRecord
|
||||
module ConnectionAdapters # :nodoc:
|
||||
module DatabaseLimits
|
||||
|
||||
# the maximum length of a table alias
|
||||
def table_alias_length
|
||||
255
|
||||
end
|
||||
|
||||
# the maximum length of a column name
|
||||
def column_name_length
|
||||
64
|
||||
end
|
||||
|
||||
# the maximum length of a table name
|
||||
def table_name_length
|
||||
64
|
||||
end
|
||||
|
||||
# the maximum length of an index name
|
||||
def index_name_length
|
||||
64
|
||||
end
|
||||
|
||||
# the maximum number of columns per table
|
||||
def columns_per_table
|
||||
1024
|
||||
end
|
||||
|
||||
# the maximum number of indexes per table
|
||||
def indexes_per_table
|
||||
16
|
||||
end
|
||||
|
||||
# the maximum number of columns in a multicolumn index
|
||||
def columns_per_multicolumn_index
|
||||
16
|
||||
end
|
||||
|
||||
# the maximum number of elements in an IN (x,y,z) clause
|
||||
def in_clause_length
|
||||
65535
|
||||
end
|
||||
|
||||
# the maximum length of a SQL query
|
||||
def sql_query_length
|
||||
1048575
|
||||
end
|
||||
|
||||
# maximum number of joins in a single query
|
||||
def joins_per_query
|
||||
256
|
||||
end
|
||||
|
||||
end
|
||||
end
|
||||
end
|
|
@ -13,7 +13,7 @@ module ActiveRecord
|
|||
|
||||
def dirties_query_cache(base, *method_names)
|
||||
method_names.each do |method_name|
|
||||
base.class_eval <<-end_code, __FILE__, __LINE__
|
||||
base.class_eval <<-end_code, __FILE__, __LINE__ + 1
|
||||
def #{method_name}_with_query_dirty(*args) # def update_with_query_dirty(*args)
|
||||
clear_query_cache if @query_cache_enabled # clear_query_cache if @query_cache_enabled
|
||||
#{method_name}_without_query_dirty(*args) # update_without_query_dirty(*args)
|
||||
|
|
|
@ -11,12 +11,12 @@ module ActiveRecord
|
|||
when String, ActiveSupport::Multibyte::Chars
|
||||
value = value.to_s
|
||||
if column && column.type == :binary && column.class.respond_to?(:string_to_binary)
|
||||
"#{quoted_string_prefix}'#{quote_string(column.class.string_to_binary(value))}'" # ' (for ruby-mode)
|
||||
"'#{quote_string(column.class.string_to_binary(value))}'" # ' (for ruby-mode)
|
||||
elsif column && [:integer, :float].include?(column.type)
|
||||
value = column.type == :integer ? value.to_i : value.to_f
|
||||
value.to_s
|
||||
else
|
||||
"#{quoted_string_prefix}'#{quote_string(value)}'" # ' (for ruby-mode)
|
||||
"'#{quote_string(value)}'" # ' (for ruby-mode)
|
||||
end
|
||||
when NilClass then "NULL"
|
||||
when TrueClass then (column && column.type == :integer ? '1' : quoted_true)
|
||||
|
@ -28,7 +28,7 @@ module ActiveRecord
|
|||
if value.acts_like?(:date) || value.acts_like?(:time)
|
||||
"'#{quoted_date(value)}'"
|
||||
else
|
||||
"#{quoted_string_prefix}'#{quote_string(value.to_yaml)}'"
|
||||
"'#{quote_string(value.to_yaml)}'"
|
||||
end
|
||||
end
|
||||
end
|
||||
|
@ -60,10 +60,6 @@ module ActiveRecord
|
|||
def quoted_date(value)
|
||||
value.to_s(:db)
|
||||
end
|
||||
|
||||
def quoted_string_prefix
|
||||
''
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
|
|
@ -256,7 +256,7 @@ module ActiveRecord
|
|||
end
|
||||
end
|
||||
|
||||
class IndexDefinition < Struct.new(:table, :name, :unique, :columns) #:nodoc:
|
||||
class IndexDefinition < Struct.new(:table, :name, :unique, :columns, :lengths) #:nodoc:
|
||||
end
|
||||
|
||||
# Abstract representation of a column definition. Instances of this type
|
||||
|
|
|
@ -8,11 +8,6 @@ module ActiveRecord
|
|||
{}
|
||||
end
|
||||
|
||||
# This is the maximum length a table alias can be
|
||||
def table_alias_length
|
||||
255
|
||||
end
|
||||
|
||||
# Truncates a table alias according to the limits of the current adapter.
|
||||
def table_alias_for(table_name)
|
||||
table_name[0..table_alias_length-1].gsub(/\./, '_')
|
||||
|
@ -101,7 +96,7 @@ module ActiveRecord
|
|||
table_definition = TableDefinition.new(self)
|
||||
table_definition.primary_key(options[:primary_key] || Base.get_primary_key(table_name.to_s.singularize)) unless options[:id] == false
|
||||
|
||||
yield table_definition
|
||||
yield table_definition if block_given?
|
||||
|
||||
if options[:force] && table_exists?(table_name)
|
||||
drop_table(table_name, options)
|
||||
|
@ -246,18 +241,32 @@ module ActiveRecord
|
|||
# name.
|
||||
#
|
||||
# ===== Examples
|
||||
#
|
||||
# ====== Creating a simple index
|
||||
# add_index(:suppliers, :name)
|
||||
# generates
|
||||
# CREATE INDEX suppliers_name_index ON suppliers(name)
|
||||
#
|
||||
# ====== Creating a unique index
|
||||
# add_index(:accounts, [:branch_id, :party_id], :unique => true)
|
||||
# generates
|
||||
# CREATE UNIQUE INDEX accounts_branch_id_party_id_index ON accounts(branch_id, party_id)
|
||||
#
|
||||
# ====== Creating a named index
|
||||
# add_index(:accounts, [:branch_id, :party_id], :unique => true, :name => 'by_branch_party')
|
||||
# generates
|
||||
# CREATE UNIQUE INDEX by_branch_party ON accounts(branch_id, party_id)
|
||||
#
|
||||
# ====== Creating an index with specific key length
|
||||
# add_index(:accounts, :name, :name => 'by_name', :length => 10)
|
||||
# generates
|
||||
# CREATE INDEX by_name ON accounts(name(10))
|
||||
#
|
||||
# add_index(:accounts, [:name, :surname], :name => 'by_name_surname', :length => {:name => 10, :surname => 15})
|
||||
# generates
|
||||
# CREATE INDEX by_name_surname ON accounts(name(10), surname(15))
|
||||
#
|
||||
# Note: SQLite doesn't support index length
|
||||
def add_index(table_name, column_name, options = {})
|
||||
column_names = Array(column_name)
|
||||
index_name = index_name(table_name, :column => column_names)
|
||||
|
@ -268,7 +277,17 @@ module ActiveRecord
|
|||
else
|
||||
index_type = options
|
||||
end
|
||||
quoted_column_names = column_names.map { |e| quote_column_name(e) }.join(", ")
|
||||
|
||||
if index_name.length > index_name_length
|
||||
@logger.warn("Index name '#{index_name}' on table '#{table_name}' is too long; the limit is #{index_name_length} characters. Skipping.")
|
||||
return
|
||||
end
|
||||
if index_exists?(table_name, index_name, false)
|
||||
@logger.warn("Index name '#{index_name}' on table '#{table_name}' already exists. Skipping.")
|
||||
return
|
||||
end
|
||||
quoted_column_names = quoted_columns_for_index(column_names, options).join(", ")
|
||||
|
||||
execute "CREATE #{index_type} INDEX #{quote_column_name(index_name)} ON #{quote_table_name(table_name)} (#{quoted_column_names})"
|
||||
end
|
||||
|
||||
|
@ -283,7 +302,28 @@ module ActiveRecord
|
|||
# Remove the index named by_branch_party in the accounts table.
|
||||
# remove_index :accounts, :name => :by_branch_party
|
||||
def remove_index(table_name, options = {})
|
||||
execute "DROP INDEX #{quote_column_name(index_name(table_name, options))} ON #{table_name}"
|
||||
index_name = index_name(table_name, options)
|
||||
unless index_exists?(table_name, index_name, true)
|
||||
@logger.warn("Index name '#{index_name}' on table '#{table_name}' does not exist. Skipping.")
|
||||
return
|
||||
end
|
||||
remove_index!(table_name, index_name)
|
||||
end
|
||||
|
||||
def remove_index!(table_name, index_name) #:nodoc:
|
||||
execute "DROP INDEX #{quote_column_name(index_name)} ON #{table_name}"
|
||||
end
|
||||
|
||||
# Rename an index.
|
||||
#
|
||||
# Rename the index_people_on_last_name index to index_users_on_last_name
|
||||
# rename_index :people, 'index_people_on_last_name', 'index_users_on_last_name'
|
||||
def rename_index(table_name, old_name, new_name)
|
||||
# this is a naive implementation; some DBs may support this more efficiently (Postgres, for instance)
|
||||
old_index_def = indexes(table_name).detect { |i| i.name == old_name }
|
||||
return unless old_index_def
|
||||
remove_index(table_name, :name => old_name)
|
||||
add_index(table_name, old_index_def.columns, :name => new_name, :unique => old_index_def.unique)
|
||||
end
|
||||
|
||||
def index_name(table_name, options) #:nodoc:
|
||||
|
@ -300,6 +340,15 @@ module ActiveRecord
|
|||
end
|
||||
end
|
||||
|
||||
# Verify the existence of an index.
|
||||
#
|
||||
# The default argument is returned if the underlying implementation does not define the indexes method,
|
||||
# as there's no way to determine the correct answer in that case.
|
||||
def index_exists?(table_name, index_name, default)
|
||||
return default unless respond_to?(:indexes)
|
||||
indexes(table_name).detect { |i| i.name == index_name }
|
||||
end
|
||||
|
||||
# Returns a string of <tt>CREATE TABLE</tt> SQL statement(s) for recreating the
|
||||
# entire structure of the database.
|
||||
def structure_dump
|
||||
|
@ -336,12 +385,12 @@ module ActiveRecord
|
|||
end
|
||||
end
|
||||
|
||||
def assume_migrated_upto_version(version)
|
||||
def assume_migrated_upto_version(version, migrations_path = ActiveRecord::Migrator.migrations_path)
|
||||
version = version.to_i
|
||||
sm_table = quote_table_name(ActiveRecord::Migrator.schema_migrations_table_name)
|
||||
|
||||
migrated = select_values("SELECT version FROM #{sm_table}").map(&:to_i)
|
||||
versions = Dir['db/migrate/[0-9]*_*.rb'].map do |filename|
|
||||
versions = Dir["#{migrations_path}/[0-9]*_*.rb"].map do |filename|
|
||||
filename.split('/').last.split('_').first.to_i
|
||||
end
|
||||
|
||||
|
@ -426,6 +475,11 @@ module ActiveRecord
|
|||
end
|
||||
|
||||
protected
|
||||
# Overridden by the mysql adapter for supporting index lengths
|
||||
def quoted_columns_for_index(column_names, options = {})
|
||||
column_names.map {|name| quote_column_name(name) }
|
||||
end
|
||||
|
||||
def options_include_default?(options)
|
||||
options.include?(:default) && !(options[:null] == false && options[:default].nil?)
|
||||
end
|
||||
|
|
|
@ -11,6 +11,7 @@ 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'
|
||||
require 'active_record/connection_adapters/abstract/database_limits'
|
||||
|
||||
module ActiveRecord
|
||||
module ConnectionAdapters # :nodoc:
|
||||
|
@ -29,6 +30,7 @@ module ActiveRecord
|
|||
# notably, the instance methods provided by SchemaStatement are very useful.
|
||||
class AbstractAdapter
|
||||
include Quoting, DatabaseStatements, SchemaStatements
|
||||
include DatabaseLimits
|
||||
include QueryCache
|
||||
include ActiveSupport::Callbacks
|
||||
define_callbacks :checkout, :checkin
|
||||
|
|
|
@ -454,10 +454,11 @@ module ActiveRecord
|
|||
if current_index != row[2]
|
||||
next if row[2] == "PRIMARY" # skip the primary key
|
||||
current_index = row[2]
|
||||
indexes << IndexDefinition.new(row[0], row[2], row[1] == "0", [])
|
||||
indexes << IndexDefinition.new(row[0], row[2], row[1] == "0", [], [])
|
||||
end
|
||||
|
||||
indexes.last.columns << row[4]
|
||||
indexes.last.lengths << row[7]
|
||||
end
|
||||
result.free
|
||||
indexes
|
||||
|
@ -480,6 +481,13 @@ module ActiveRecord
|
|||
execute "RENAME TABLE #{quote_table_name(table_name)} TO #{quote_table_name(new_name)}"
|
||||
end
|
||||
|
||||
def add_column(table_name, column_name, type, options = {})
|
||||
add_column_sql = "ALTER TABLE #{quote_table_name(table_name)} ADD #{quote_column_name(column_name)} #{type_to_sql(type, options[:limit], options[:precision], options[:scale])}"
|
||||
add_column_options!(add_column_sql, options)
|
||||
add_column_position!(add_column_sql, options)
|
||||
execute(add_column_sql)
|
||||
end
|
||||
|
||||
def change_column_default(table_name, column_name, default) #:nodoc:
|
||||
column = column_for(table_name, column_name)
|
||||
change_column table_name, column_name, column.sql_type, :default => default
|
||||
|
@ -508,6 +516,7 @@ module ActiveRecord
|
|||
|
||||
change_column_sql = "ALTER TABLE #{quote_table_name(table_name)} CHANGE #{quote_column_name(column_name)} #{quote_column_name(column_name)} #{type_to_sql(type, options[:limit], options[:precision], options[:scale])}"
|
||||
add_column_options!(change_column_sql, options)
|
||||
add_column_position!(change_column_sql, options)
|
||||
execute(change_column_sql)
|
||||
end
|
||||
|
||||
|
@ -539,6 +548,13 @@ module ActiveRecord
|
|||
end
|
||||
end
|
||||
|
||||
def add_column_position!(sql, options)
|
||||
if options[:first]
|
||||
sql << " FIRST"
|
||||
elsif options[:after]
|
||||
sql << " AFTER #{quote_column_name(options[:after])}"
|
||||
end
|
||||
end
|
||||
|
||||
# SHOW VARIABLES LIKE 'name'
|
||||
def show_variable(name)
|
||||
|
@ -571,6 +587,20 @@ module ActiveRecord
|
|||
where_sql
|
||||
end
|
||||
|
||||
protected
|
||||
def quoted_columns_for_index(column_names, options = {})
|
||||
length = options[:length] if options.is_a?(Hash)
|
||||
|
||||
quoted_column_names = case length
|
||||
when Hash
|
||||
column_names.map {|name| length[name] ? "#{quote_column_name(name)}(#{length[name]})" : quote_column_name(name) }
|
||||
when Fixnum
|
||||
column_names.map {|name| "#{quote_column_name(name)}(#{length})"}
|
||||
else
|
||||
column_names.map {|name| quote_column_name(name) }
|
||||
end
|
||||
end
|
||||
|
||||
private
|
||||
def connect
|
||||
encoding = @config[:encoding]
|
||||
|
|
|
@ -261,20 +261,12 @@ module ActiveRecord
|
|||
true
|
||||
end
|
||||
|
||||
# Does PostgreSQL support standard conforming strings?
|
||||
def supports_standard_conforming_strings?
|
||||
# Temporarily set the client message level above error to prevent unintentional
|
||||
# error messages in the logs when working on a PostgreSQL database server that
|
||||
# does not support standard conforming strings.
|
||||
client_min_messages_old = client_min_messages
|
||||
self.client_min_messages = 'panic'
|
||||
|
||||
# postgres-pr does not raise an exception when client_min_messages is set higher
|
||||
# than error and "SHOW standard_conforming_strings" fails, but returns an empty
|
||||
# PGresult instead.
|
||||
has_support = query('SHOW standard_conforming_strings')[0][0] rescue false
|
||||
self.client_min_messages = client_min_messages_old
|
||||
has_support
|
||||
# Enable standard-conforming strings if available.
|
||||
def set_standard_conforming_strings
|
||||
old, self.client_min_messages = client_min_messages, 'panic'
|
||||
execute('SET standard_conforming_strings = on') rescue nil
|
||||
ensure
|
||||
self.client_min_messages = old
|
||||
end
|
||||
|
||||
def supports_insert_with_returning?
|
||||
|
@ -298,7 +290,7 @@ module ActiveRecord
|
|||
# QUOTING ==================================================
|
||||
|
||||
# Escapes binary strings for bytea input to the database.
|
||||
def escape_bytea(value)
|
||||
def escape_bytea(original_value)
|
||||
if @connection.respond_to?(:escape_bytea)
|
||||
self.class.instance_eval do
|
||||
define_method(:escape_bytea) do |value|
|
||||
|
@ -322,62 +314,40 @@ module ActiveRecord
|
|||
end
|
||||
end
|
||||
end
|
||||
escape_bytea(value)
|
||||
escape_bytea(original_value)
|
||||
end
|
||||
|
||||
# Unescapes bytea output from a database to the binary string it represents.
|
||||
# NOTE: This is NOT an inverse of escape_bytea! This is only to be used
|
||||
# on escaped binary output from database drive.
|
||||
def unescape_bytea(value)
|
||||
def unescape_bytea(original_value)
|
||||
# In each case, check if the value actually is escaped PostgreSQL bytea output
|
||||
# or an unescaped Active Record attribute that was just written.
|
||||
if PGconn.respond_to?(:unescape_bytea)
|
||||
if @connection.respond_to?(:unescape_bytea)
|
||||
self.class.instance_eval do
|
||||
define_method(:unescape_bytea) do |value|
|
||||
if value =~ /\\\d{3}/
|
||||
PGconn.unescape_bytea(value)
|
||||
else
|
||||
value
|
||||
end
|
||||
@connection.unescape_bytea(value) if value
|
||||
end
|
||||
end
|
||||
elsif PGconn.respond_to?(:unescape_bytea)
|
||||
self.class.instance_eval do
|
||||
define_method(:unescape_bytea) do |value|
|
||||
PGconn.unescape_bytea(value) if value
|
||||
end
|
||||
end
|
||||
else
|
||||
self.class.instance_eval do
|
||||
define_method(:unescape_bytea) do |value|
|
||||
if value =~ /\\\d{3}/
|
||||
result = ''
|
||||
i, max = 0, value.size
|
||||
while i < max
|
||||
char = value[i]
|
||||
if char == ?\\
|
||||
if value[i+1] == ?\\
|
||||
char = ?\\
|
||||
i += 1
|
||||
else
|
||||
char = value[i+1..i+3].oct
|
||||
i += 3
|
||||
end
|
||||
end
|
||||
result << char
|
||||
i += 1
|
||||
end
|
||||
result
|
||||
else
|
||||
value
|
||||
end
|
||||
end
|
||||
end
|
||||
raise 'Your PostgreSQL connection does not support unescape_bytea. Try upgrading to pg 0.9.0 or later.'
|
||||
end
|
||||
unescape_bytea(value)
|
||||
unescape_bytea(original_value)
|
||||
end
|
||||
|
||||
# Quotes PostgreSQL-specific data types for SQL input.
|
||||
def quote(value, column = nil) #:nodoc:
|
||||
if value.kind_of?(String) && column && column.type == :binary
|
||||
"#{quoted_string_prefix}'#{escape_bytea(value)}'"
|
||||
elsif value.kind_of?(String) && column && column.sql_type =~ /^xml$/
|
||||
"xml E'#{quote_string(value)}'"
|
||||
elsif value.kind_of?(Numeric) && column && column.sql_type =~ /^money$/
|
||||
"'#{escape_bytea(value)}'"
|
||||
elsif value.kind_of?(String) && column && column.sql_type == 'xml'
|
||||
"xml '#{quote_string(value)}'"
|
||||
elsif value.kind_of?(Numeric) && column && column.sql_type == 'money'
|
||||
# Not truly string input, so doesn't require (or allow) escape string syntax.
|
||||
"'#{value.to_s}'"
|
||||
elsif value.kind_of?(String) && column && column.sql_type =~ /^bit/
|
||||
|
@ -393,7 +363,7 @@ module ActiveRecord
|
|||
end
|
||||
|
||||
# Quotes strings for use in SQL input in the postgres driver for better performance.
|
||||
def quote_string(s) #:nodoc:
|
||||
def quote_string(original_value) #:nodoc:
|
||||
if @connection.respond_to?(:escape)
|
||||
self.class.instance_eval do
|
||||
define_method(:quote_string) do |s|
|
||||
|
@ -413,7 +383,7 @@ module ActiveRecord
|
|||
remove_method(:quote_string)
|
||||
end
|
||||
end
|
||||
quote_string(s)
|
||||
quote_string(original_value)
|
||||
end
|
||||
|
||||
# Checks the following cases:
|
||||
|
@ -889,9 +859,12 @@ module ActiveRecord
|
|||
execute "ALTER TABLE #{quote_table_name(table_name)} RENAME COLUMN #{quote_column_name(column_name)} TO #{quote_column_name(new_column_name)}"
|
||||
end
|
||||
|
||||
# Drops an index from a table.
|
||||
def remove_index(table_name, options = {})
|
||||
execute "DROP INDEX #{quote_table_name(index_name(table_name, options))}"
|
||||
def remove_index!(table_name, index_name) #:nodoc:
|
||||
execute "DROP INDEX #{quote_table_name(index_name)}"
|
||||
end
|
||||
|
||||
def index_name_length
|
||||
63
|
||||
end
|
||||
|
||||
# Maps logical Rails types to PostgreSQL-specific data types.
|
||||
|
@ -971,17 +944,6 @@ module ActiveRecord
|
|||
# Ignore async_exec and async_query when using postgres-pr.
|
||||
@async = @config[:allow_concurrency] && @connection.respond_to?(:async_exec)
|
||||
|
||||
# Use escape string syntax if available. We cannot do this lazily when encountering
|
||||
# the first string, because that could then break any transactions in progress.
|
||||
# See: http://www.postgresql.org/docs/current/static/runtime-config-compatible.html
|
||||
# If PostgreSQL doesn't know the standard_conforming_strings parameter then it doesn't
|
||||
# support escape string syntax. Don't override the inherited quoted_string_prefix.
|
||||
if supports_standard_conforming_strings?
|
||||
self.class.instance_eval do
|
||||
define_method(:quoted_string_prefix) { 'E' }
|
||||
end
|
||||
end
|
||||
|
||||
# Money type has a fixed precision of 10 in PostgreSQL 8.2 and below, and as of
|
||||
# PostgreSQL 8.3 it has a fixed precision of 19. PostgreSQLColumn.extract_precision
|
||||
# should know about this but can't detect it there, so deal with it here.
|
||||
|
@ -1011,6 +973,9 @@ module ActiveRecord
|
|||
end
|
||||
self.client_min_messages = @config[:min_messages] if @config[:min_messages]
|
||||
self.schema_search_path = @config[:schema_search_path] || @config[:schema_order]
|
||||
|
||||
# Use standard-conforming strings if available so we don't have to do the E'...' dance.
|
||||
set_standard_conforming_strings
|
||||
end
|
||||
|
||||
# Returns the current ID of a table's sequence.
|
||||
|
|
|
@ -220,20 +220,20 @@ module ActiveRecord
|
|||
SQL
|
||||
|
||||
execute(sql, name).map do |row|
|
||||
row[0]
|
||||
row['name']
|
||||
end
|
||||
end
|
||||
|
||||
def columns(table_name, name = nil) #:nodoc:
|
||||
table_structure(table_name).map do |field|
|
||||
SQLiteColumn.new(field['name'], field['dflt_value'], field['type'], field['notnull'] == "0")
|
||||
SQLiteColumn.new(field['name'], field['dflt_value'], field['type'], field['notnull'].to_i == 0)
|
||||
end
|
||||
end
|
||||
|
||||
def indexes(table_name, name = nil) #:nodoc:
|
||||
execute("PRAGMA index_list(#{quote_table_name(table_name)})", name).map do |row|
|
||||
index = IndexDefinition.new(table_name, row['name'])
|
||||
index.unique = row['unique'] != '0'
|
||||
index.unique = row['unique'].to_i != 0
|
||||
index.columns = execute("PRAGMA index_info('#{index.name}')").map { |col| col['name'] }
|
||||
index
|
||||
end
|
||||
|
@ -244,8 +244,8 @@ module ActiveRecord
|
|||
column ? column['name'] : nil
|
||||
end
|
||||
|
||||
def remove_index(table_name, options={}) #:nodoc:
|
||||
execute "DROP INDEX #{quote_column_name(index_name(table_name, options))}"
|
||||
def remove_index!(table_name, index_name) #:nodoc:
|
||||
execute "DROP INDEX #{quote_column_name(index_name)}"
|
||||
end
|
||||
|
||||
def rename_table(name, new_name)
|
||||
|
|
|
@ -167,13 +167,13 @@ module ActiveRecord
|
|||
|
||||
module ClassMethods
|
||||
def self.extended(base)
|
||||
base.metaclass.alias_method_chain(:alias_attribute, :dirty)
|
||||
base.singleton_class.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
|
||||
module_eval <<-STR, __FILE__, __LINE__ + 1
|
||||
def #{new_name}#{suffix}; self.#{old_name}#{suffix}; end # def subject_changed?; self.title_changed?; end
|
||||
STR
|
||||
end
|
||||
|
|
|
@ -891,6 +891,7 @@ module ActiveRecord
|
|||
|
||||
instances.size == 1 ? instances.first : instances
|
||||
end
|
||||
private table_name
|
||||
end
|
||||
end
|
||||
|
||||
|
|
|
@ -1,26 +0,0 @@
|
|||
# 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) && !values.empty?
|
||||
|
||||
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
|
|
@ -23,6 +23,16 @@ module ActiveRecord
|
|||
# p2.first_name = "should fail"
|
||||
# p2.save # Raises a ActiveRecord::StaleObjectError
|
||||
#
|
||||
# Optimistic locking will also check for stale data when objects are destroyed. Example:
|
||||
#
|
||||
# p1 = Person.find(1)
|
||||
# p2 = Person.find(1)
|
||||
#
|
||||
# p1.first_name = "Michael"
|
||||
# p1.save
|
||||
#
|
||||
# p2.destroy # Raises a ActiveRecord::StaleObjectError
|
||||
#
|
||||
# You're then responsible for dealing with the conflict by rescuing the exception and either rolling back, merging,
|
||||
# or otherwise apply the business logic needed to resolve the conflict.
|
||||
#
|
||||
|
@ -39,6 +49,7 @@ module ActiveRecord
|
|||
base.lock_optimistically = true
|
||||
|
||||
base.alias_method_chain :update, :lock
|
||||
base.alias_method_chain :destroy, :lock
|
||||
base.alias_method_chain :attributes_from_column_definition, :lock
|
||||
|
||||
class << base
|
||||
|
@ -86,7 +97,7 @@ module ActiveRecord
|
|||
end_sql
|
||||
|
||||
unless affected_rows == 1
|
||||
raise ActiveRecord::StaleObjectError, "Attempted to update a stale object"
|
||||
raise ActiveRecord::StaleObjectError, "Attempted to update a stale object: #{self.class.name}"
|
||||
end
|
||||
|
||||
affected_rows
|
||||
|
@ -98,6 +109,28 @@ module ActiveRecord
|
|||
end
|
||||
end
|
||||
|
||||
def destroy_with_lock #:nodoc:
|
||||
return destroy_without_lock unless locking_enabled?
|
||||
|
||||
unless new_record?
|
||||
lock_col = self.class.locking_column
|
||||
previous_value = send(lock_col).to_i
|
||||
|
||||
affected_rows = connection.delete(
|
||||
"DELETE FROM #{self.class.quoted_table_name} " +
|
||||
"WHERE #{connection.quote_column_name(self.class.primary_key)} = #{quoted_id} " +
|
||||
"AND #{self.class.quoted_locking_column} = #{quote_value(previous_value)}",
|
||||
"#{self.class.name} Destroy"
|
||||
)
|
||||
|
||||
unless affected_rows == 1
|
||||
raise ActiveRecord::StaleObjectError, "Attempted to delete a stale object: #{self.class.name}"
|
||||
end
|
||||
end
|
||||
|
||||
freeze
|
||||
end
|
||||
|
||||
module ClassMethods
|
||||
DEFAULT_LOCKING_COLUMN = 'lock_version'
|
||||
|
||||
|
|
|
@ -381,6 +381,7 @@ module ActiveRecord
|
|||
def migrate(migrations_path, target_version = nil)
|
||||
case
|
||||
when target_version.nil? then up(migrations_path, target_version)
|
||||
when current_version == 0 && target_version == 0 then # noop
|
||||
when current_version > target_version then down(migrations_path, target_version)
|
||||
else up(migrations_path, target_version)
|
||||
end
|
||||
|
@ -408,6 +409,10 @@ module ActiveRecord
|
|||
self.new(direction, migrations_path, target_version).run
|
||||
end
|
||||
|
||||
def migrations_path
|
||||
'db/migrate'
|
||||
end
|
||||
|
||||
def schema_migrations_table_name
|
||||
Base.table_name_prefix + 'schema_migrations' + Base.table_name_suffix
|
||||
end
|
||||
|
|
|
@ -184,6 +184,8 @@ module ActiveRecord
|
|||
# the parent model is saved. This happens inside the transaction initiated
|
||||
# by the parents save method. See ActiveRecord::AutosaveAssociation.
|
||||
module ClassMethods
|
||||
REJECT_ALL_BLANK_PROC = proc { |attributes| attributes.all? { |_, value| value.blank? } }
|
||||
|
||||
# Defines an attributes writer for the specified association(s). If you
|
||||
# are using <tt>attr_protected</tt> or <tt>attr_accessible</tt>, then you
|
||||
# will need to add the attribute writer to the allowed list.
|
||||
|
@ -208,39 +210,40 @@ module ActiveRecord
|
|||
# nested attributes array exceeds the specified limit, NestedAttributes::TooManyRecords
|
||||
# exception is raised. If omitted, any number associations can be processed.
|
||||
# Note that the :limit option is only applicable to one-to-many associations.
|
||||
# [:update_only]
|
||||
# Allows you to specify that an existing record may only be updated.
|
||||
# A new record may only be created when there is no existing record.
|
||||
# This option only works for one-to-one associations and is ignored for
|
||||
# collection associations. This option is off by default.
|
||||
#
|
||||
# Examples:
|
||||
# # creates avatar_attributes=
|
||||
# accepts_nested_attributes_for :avatar, :reject_if => proc { |attributes| attributes['name'].blank? }
|
||||
# # creates avatar_attributes=
|
||||
# accepts_nested_attributes_for :avatar, :reject_if => :all_blank
|
||||
# # creates avatar_attributes= and posts_attributes=
|
||||
# accepts_nested_attributes_for :avatar, :posts, :allow_destroy => true
|
||||
def accepts_nested_attributes_for(*attr_names)
|
||||
options = { :allow_destroy => false }
|
||||
options = { :allow_destroy => false, :update_only => false }
|
||||
options.update(attr_names.extract_options!)
|
||||
options.assert_valid_keys(:allow_destroy, :reject_if, :limit)
|
||||
options.assert_valid_keys(:allow_destroy, :reject_if, :limit, :update_only)
|
||||
options[:reject_if] = REJECT_ALL_BLANK_PROC if options[:reject_if] == :all_blank
|
||||
|
||||
attr_names.each do |association_name|
|
||||
if reflection = reflect_on_association(association_name)
|
||||
type = case reflection.macro
|
||||
when :has_one, :belongs_to
|
||||
:one_to_one
|
||||
when :has_many, :has_and_belongs_to_many
|
||||
:collection
|
||||
end
|
||||
|
||||
reflection.options[:autosave] = true
|
||||
self.nested_attributes_options[association_name.to_sym] = options
|
||||
add_autosave_association_callbacks(reflection)
|
||||
nested_attributes_options[association_name.to_sym] = options
|
||||
type = (reflection.collection? ? :collection : :one_to_one)
|
||||
|
||||
# def pirate_attributes=(attributes)
|
||||
# assign_nested_attributes_for_one_to_one_association(:pirate, attributes, false)
|
||||
# assign_nested_attributes_for_one_to_one_association(:pirate, attributes)
|
||||
# end
|
||||
class_eval %{
|
||||
class_eval <<-EOS, __FILE__, __LINE__ + 1
|
||||
def #{association_name}_attributes=(attributes)
|
||||
assign_nested_attributes_for_#{type}_association(:#{association_name}, attributes)
|
||||
end
|
||||
}, __FILE__, __LINE__
|
||||
|
||||
add_autosave_association_callbacks(reflection)
|
||||
EOS
|
||||
else
|
||||
raise ArgumentError, "No association found for name `#{association_name}'. Has it been defined yet?"
|
||||
end
|
||||
|
@ -257,46 +260,41 @@ module ActiveRecord
|
|||
marked_for_destruction?
|
||||
end
|
||||
|
||||
# Deal with deprecated _delete.
|
||||
#
|
||||
def _delete #:nodoc:
|
||||
ActiveSupport::Deprecation.warn "_delete is deprecated in nested attributes. Use _destroy instead."
|
||||
_destroy
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
# Attribute hash keys that should not be assigned as normal attributes.
|
||||
# These hash keys are nested attributes implementation details.
|
||||
#
|
||||
# TODO Remove _delete from UNASSIGNABLE_KEYS when deprecation warning are
|
||||
# removed.
|
||||
UNASSIGNABLE_KEYS = %w( id _destroy _delete )
|
||||
UNASSIGNABLE_KEYS = %w( id _destroy )
|
||||
|
||||
# Assigns the given attributes to the association.
|
||||
#
|
||||
# If the given attributes include an <tt>:id</tt> that matches the existing
|
||||
# record’s id, then the existing record will be modified. Otherwise a new
|
||||
# record will be built.
|
||||
# If update_only is false and the given attributes include an <tt>:id</tt>
|
||||
# that matches the existing record’s id, then the existing record will be
|
||||
# modified. If update_only is true, a new record is only created when no
|
||||
# object exists. Otherwise a new record will be built.
|
||||
#
|
||||
# If the given attributes include a matching <tt>:id</tt> attribute _and_ a
|
||||
# <tt>:_destroy</tt> key set to a truthy value, then the existing record
|
||||
# will be marked for destruction.
|
||||
# If the given attributes include a matching <tt>:id</tt> attribute, or
|
||||
# update_only is true, and a <tt>:_destroy</tt> key set to a truthy value,
|
||||
# then the existing record will be marked for destruction.
|
||||
def assign_nested_attributes_for_one_to_one_association(association_name, attributes)
|
||||
options = self.nested_attributes_options[association_name]
|
||||
options = nested_attributes_options[association_name]
|
||||
attributes = attributes.with_indifferent_access
|
||||
check_existing_record = (options[:update_only] || !attributes['id'].blank?)
|
||||
|
||||
if attributes['id'].blank?
|
||||
unless reject_new_record?(association_name, attributes)
|
||||
method = "build_#{association_name}"
|
||||
if respond_to?(method)
|
||||
send(method, attributes.except(*UNASSIGNABLE_KEYS))
|
||||
else
|
||||
raise ArgumentError, "Cannot build association #{association_name}. Are you trying to build a polymorphic one-to-one association?"
|
||||
end
|
||||
if check_existing_record && (record = send(association_name)) &&
|
||||
(options[:update_only] || record.id.to_s == attributes['id'].to_s)
|
||||
assign_to_or_mark_for_destruction(record, attributes, options[:allow_destroy])
|
||||
|
||||
elsif attributes['id']
|
||||
raise_nested_attributes_record_not_found(association_name, attributes['id'])
|
||||
|
||||
elsif !reject_new_record?(association_name, attributes)
|
||||
method = "build_#{association_name}"
|
||||
if respond_to?(method)
|
||||
send(method, attributes.except(*UNASSIGNABLE_KEYS))
|
||||
else
|
||||
raise ArgumentError, "Cannot build association #{association_name}. Are you trying to build a polymorphic one-to-one association?"
|
||||
end
|
||||
elsif (existing_record = send(association_name)) && existing_record.id.to_s == attributes['id'].to_s
|
||||
assign_to_or_mark_for_destruction(existing_record, attributes, options[:allow_destroy])
|
||||
end
|
||||
end
|
||||
|
||||
|
@ -328,7 +326,7 @@ module ActiveRecord
|
|||
# { :id => '2', :_destroy => true }
|
||||
# ])
|
||||
def assign_nested_attributes_for_collection_association(association_name, attributes_collection)
|
||||
options = self.nested_attributes_options[association_name]
|
||||
options = nested_attributes_options[association_name]
|
||||
|
||||
unless attributes_collection.is_a?(Hash) || attributes_collection.is_a?(Array)
|
||||
raise ArgumentError, "Hash or Array expected, got #{attributes_collection.class.name} (#{attributes_collection.inspect})"
|
||||
|
@ -342,15 +340,27 @@ module ActiveRecord
|
|||
attributes_collection = attributes_collection.sort_by { |index, _| index.to_i }.map { |_, attributes| attributes }
|
||||
end
|
||||
|
||||
association = send(association_name)
|
||||
|
||||
existing_records = if association.loaded?
|
||||
association.to_a
|
||||
else
|
||||
attribute_ids = attributes_collection.map {|a| a['id'] || a[:id] }.compact
|
||||
attribute_ids.present? ? association.all(:conditions => {association.primary_key => attribute_ids}) : []
|
||||
end
|
||||
|
||||
attributes_collection.each do |attributes|
|
||||
attributes = attributes.with_indifferent_access
|
||||
|
||||
if attributes['id'].blank?
|
||||
unless reject_new_record?(association_name, attributes)
|
||||
send(association_name).build(attributes.except(*UNASSIGNABLE_KEYS))
|
||||
association.build(attributes.except(*UNASSIGNABLE_KEYS))
|
||||
end
|
||||
elsif existing_record = send(association_name).detect { |record| record.id.to_s == attributes['id'].to_s }
|
||||
elsif existing_record = existing_records.detect { |record| record.id.to_s == attributes['id'].to_s }
|
||||
association.send(:add_record_to_target_with_callbacks, existing_record) unless association.loaded?
|
||||
assign_to_or_mark_for_destruction(existing_record, attributes, options[:allow_destroy])
|
||||
else
|
||||
raise_nested_attributes_record_not_found(association_name, attributes['id'])
|
||||
end
|
||||
end
|
||||
end
|
||||
|
@ -367,8 +377,7 @@ module ActiveRecord
|
|||
|
||||
# Determines if a hash contains a truthy _destroy key.
|
||||
def has_destroy_flag?(hash)
|
||||
ConnectionAdapters::Column.value_to_boolean(hash['_destroy']) ||
|
||||
ConnectionAdapters::Column.value_to_boolean(hash['_delete']) # TODO Remove after deprecation.
|
||||
ConnectionAdapters::Column.value_to_boolean(hash['_destroy'])
|
||||
end
|
||||
|
||||
# Determines if a new record should be build by checking for
|
||||
|
@ -379,14 +388,17 @@ module ActiveRecord
|
|||
end
|
||||
|
||||
def call_reject_if(association_name, attributes)
|
||||
callback = self.nested_attributes_options[association_name][:reject_if]
|
||||
|
||||
case callback
|
||||
case callback = nested_attributes_options[association_name][:reject_if]
|
||||
when Symbol
|
||||
method(callback).arity == 0 ? send(callback) : send(callback, attributes)
|
||||
when Proc
|
||||
callback.try(:call, attributes)
|
||||
callback.call(attributes)
|
||||
end
|
||||
end
|
||||
|
||||
def raise_nested_attributes_record_not_found(association_name, record_id)
|
||||
reflection = self.class.reflect_on_association(association_name)
|
||||
raise RecordNotFound, "Couldn't find #{reflection.klass.name} with ID=#{record_id} for #{self.class.name} with ID=#{id}"
|
||||
end
|
||||
end
|
||||
end
|
||||
|
|
|
@ -212,6 +212,15 @@ module ActiveRecord
|
|||
end
|
||||
|
||||
def check_validity!
|
||||
check_validity_of_inverse!
|
||||
end
|
||||
|
||||
def check_validity_of_inverse!
|
||||
unless options[:polymorphic]
|
||||
if has_inverse? && inverse_of.nil?
|
||||
raise InverseOfAssociationNotFoundError.new(self)
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
def through_reflection
|
||||
|
@ -225,10 +234,64 @@ module ActiveRecord
|
|||
nil
|
||||
end
|
||||
|
||||
def has_inverse?
|
||||
!@options[:inverse_of].nil?
|
||||
end
|
||||
|
||||
def inverse_of
|
||||
if has_inverse?
|
||||
@inverse_of ||= klass.reflect_on_association(options[:inverse_of])
|
||||
end
|
||||
end
|
||||
|
||||
def polymorphic_inverse_of(associated_class)
|
||||
if has_inverse?
|
||||
if inverse_relationship = associated_class.reflect_on_association(options[:inverse_of])
|
||||
inverse_relationship
|
||||
else
|
||||
raise InverseOfAssociationNotFoundError.new(self, associated_class)
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
# Returns whether or not this association reflection is for a collection
|
||||
# association. Returns +true+ if the +macro+ is one of +has_many+ or
|
||||
# +has_and_belongs_to_many+, +false+ otherwise.
|
||||
def collection?
|
||||
if @collection.nil?
|
||||
@collection = [:has_many, :has_and_belongs_to_many].include?(macro)
|
||||
end
|
||||
@collection
|
||||
end
|
||||
|
||||
# Returns whether or not the association should be validated as part of
|
||||
# the parent's validation.
|
||||
#
|
||||
# Unless you explicitely disable validation with
|
||||
# <tt>:validate => false</tt>, it will take place when:
|
||||
#
|
||||
# * you explicitely enable validation; <tt>:validate => true</tt>
|
||||
# * you use autosave; <tt>:autosave => true</tt>
|
||||
# * the association is a +has_many+ association
|
||||
def validate?
|
||||
!options[:validate].nil? ? options[:validate] : (options[:autosave] == true || macro == :has_many)
|
||||
end
|
||||
|
||||
def dependent_conditions(record, base_class, extra_conditions)
|
||||
dependent_conditions = []
|
||||
dependent_conditions << "#{primary_key_name} = #{record.send(name).send(:owner_quoted_id)}"
|
||||
dependent_conditions << "#{options[:as]}_type = '#{base_class.name}'" if options[:as]
|
||||
dependent_conditions << klass.send(:sanitize_sql, options[:conditions]) if options[:conditions]
|
||||
dependent_conditions = dependent_conditions.collect {|where| "(#{where})" }.join(" AND ")
|
||||
dependent_conditions << extra_conditions if extra_conditions
|
||||
dependent_conditions = dependent_conditions.gsub('@', '\@')
|
||||
dependent_conditions
|
||||
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 = class_name.singularize if collection?
|
||||
class_name
|
||||
end
|
||||
|
||||
|
@ -300,6 +363,8 @@ module ActiveRecord
|
|||
unless [:belongs_to, :has_many, :has_one].include?(source_reflection.macro) && source_reflection.options[:through].nil?
|
||||
raise HasManyThroughSourceAssociationMacroError.new(self)
|
||||
end
|
||||
|
||||
check_validity_of_inverse!
|
||||
end
|
||||
|
||||
def through_reflection_primary_key
|
||||
|
|
|
@ -28,6 +28,10 @@ module ActiveRecord
|
|||
class Schema < Migration
|
||||
private_class_method :new
|
||||
|
||||
def self.migrations_path
|
||||
ActiveRecord::Migrator.migrations_path
|
||||
end
|
||||
|
||||
# Eval the given block. All methods available to the current connection
|
||||
# adapter are available within the block, so you can easily use the
|
||||
# database definition DSL to build up your schema (+create_table+,
|
||||
|
@ -44,7 +48,7 @@ module ActiveRecord
|
|||
|
||||
unless info[:version].blank?
|
||||
initialize_schema_migrations_table
|
||||
assume_migrated_upto_version info[:version]
|
||||
assume_migrated_upto_version(info[:version], migrations_path)
|
||||
end
|
||||
end
|
||||
end
|
||||
|
|
|
@ -171,6 +171,9 @@ HEADER
|
|||
statment_parts << (':name => ' + index.name.inspect)
|
||||
statment_parts << ':unique => true' if index.unique
|
||||
|
||||
index_lengths = index.lengths.compact if index.lengths.is_a?(Array)
|
||||
statment_parts << (':length => ' + Hash[*index.columns.zip(index.lengths).flatten].inspect) if index_lengths.present?
|
||||
|
||||
' ' + statment_parts.join(', ')
|
||||
end
|
||||
|
||||
|
|
|
@ -79,7 +79,7 @@ module ActiveRecord #:nodoc:
|
|||
|
||||
def as_json(options = nil) #:nodoc:
|
||||
hash = Serializer.new(self, options).serializable_record
|
||||
hash = { self.class.model_name.element => hash } if include_root_in_json
|
||||
hash = { options[:root] || self.class.model_name.element => hash } if include_root_in_json
|
||||
hash
|
||||
end
|
||||
|
||||
|
|
|
@ -158,7 +158,7 @@ module ActiveRecord
|
|||
# If +message+ is a Symbol, it will be translated, using the appropriate scope (see translate_error).
|
||||
#
|
||||
def add(attribute, message = nil, options = {})
|
||||
options[:message] = options.delete(:default) if options.has_key?(:default)
|
||||
options[:message] = options.delete(:default) if options[:default].is_a?(Symbol)
|
||||
error, message = message, nil if message.is_a?(Error)
|
||||
|
||||
@errors[attribute.to_s] ||= []
|
||||
|
@ -239,6 +239,18 @@ module ActiveRecord
|
|||
@errors.each_key { |attr| @errors[attr].each { |error| yield attr, error.message } }
|
||||
end
|
||||
|
||||
# Yields each attribute and associated error per error added.
|
||||
#
|
||||
# class Company < ActiveRecord::Base
|
||||
# validates_presence_of :name, :address, :email
|
||||
# validates_length_of :name, :in => 5..30
|
||||
# end
|
||||
#
|
||||
# company = Company.create(:address => '123 First St.')
|
||||
# company.errors.each_error{|attr,err| puts "#{attr} - #{err.type}" }
|
||||
# # => name - :too_short
|
||||
# # name - :blank
|
||||
# # address - :blank
|
||||
def each_error
|
||||
@errors.each_key { |attr| @errors[attr].each { |error| yield attr, error } }
|
||||
end
|
||||
|
|
|
@ -2,7 +2,7 @@ module ActiveRecord
|
|||
module VERSION #:nodoc:
|
||||
MAJOR = 2
|
||||
MINOR = 3
|
||||
TINY = 5
|
||||
TINY = 8
|
||||
|
||||
STRING = [MAJOR, MINOR, TINY].join('.')
|
||||
end
|
||||
|
|
|
@ -15,6 +15,28 @@ class ActiveSchemaTest < ActiveRecord::TestCase
|
|||
end
|
||||
end
|
||||
|
||||
def test_add_index
|
||||
# add_index calls index_exists? which can't work since execute is stubbed
|
||||
ActiveRecord::ConnectionAdapters::MysqlAdapter.send(:define_method, :index_exists?) do |*|
|
||||
false
|
||||
end
|
||||
expected = "CREATE INDEX `index_people_on_last_name` ON `people` (`last_name`)"
|
||||
assert_equal expected, add_index(:people, :last_name, :length => nil)
|
||||
|
||||
expected = "CREATE INDEX `index_people_on_last_name` ON `people` (`last_name`(10))"
|
||||
assert_equal expected, add_index(:people, :last_name, :length => 10)
|
||||
|
||||
expected = "CREATE INDEX `index_people_on_last_name_and_first_name` ON `people` (`last_name`(15), `first_name`(15))"
|
||||
assert_equal expected, add_index(:people, [:last_name, :first_name], :length => 15)
|
||||
|
||||
expected = "CREATE INDEX `index_people_on_last_name_and_first_name` ON `people` (`last_name`(15), `first_name`)"
|
||||
assert_equal expected, add_index(:people, [:last_name, :first_name], :length => {:last_name => 15})
|
||||
|
||||
expected = "CREATE INDEX `index_people_on_last_name_and_first_name` ON `people` (`last_name`(15), `first_name`(10))"
|
||||
assert_equal expected, add_index(:people, [:last_name, :first_name], :length => {:last_name => 15, :first_name => 10})
|
||||
ActiveRecord::ConnectionAdapters::MysqlAdapter.send(:remove_method, :index_exists?)
|
||||
end
|
||||
|
||||
def test_drop_table
|
||||
assert_equal "DROP TABLE `people`", drop_table(:people)
|
||||
end
|
||||
|
|
|
@ -31,6 +31,12 @@ class BelongsToAssociationsTest < ActiveRecord::TestCase
|
|||
assert_equal companies(:first_firm).name, client.firm_with_primary_key.name
|
||||
end
|
||||
|
||||
def test_belongs_to_with_primary_key_joins_on_correct_column
|
||||
sql = Client.send(:construct_finder_sql, :joins => :firm_with_primary_key)
|
||||
assert sql !~ /\.id/
|
||||
assert sql =~ /\.name/
|
||||
end
|
||||
|
||||
def test_proxy_assignment
|
||||
account = Account.find(1)
|
||||
assert_nothing_raised { account.firm = account.firm }
|
||||
|
@ -60,6 +66,13 @@ class BelongsToAssociationsTest < ActiveRecord::TestCase
|
|||
assert_equal apple.name, citibank.firm_name
|
||||
end
|
||||
|
||||
def test_eager_loading_with_primary_key
|
||||
apple = Firm.create("name" => "Apple")
|
||||
citibank = Client.create("name" => "Citibank", :firm_name => "Apple")
|
||||
citibank_result = Client.find(:first, :conditions => {:name => "Citibank"}, :include => :firm_with_primary_key)
|
||||
assert_not_nil citibank_result.instance_variable_get("@firm_with_primary_key")
|
||||
end
|
||||
|
||||
def test_no_unexpected_aliasing
|
||||
first_firm = companies(:first_firm)
|
||||
another_firm = companies(:another_firm)
|
||||
|
|
|
@ -4,6 +4,7 @@ require 'models/author'
|
|||
require 'models/comment'
|
||||
require 'models/category'
|
||||
require 'models/categorization'
|
||||
require 'active_support/core_ext/array/random_access'
|
||||
|
||||
module Remembered
|
||||
def self.included(base)
|
||||
|
@ -17,7 +18,7 @@ module Remembered
|
|||
|
||||
module ClassMethods
|
||||
def remembered; @@remembered ||= []; end
|
||||
def rand; @@remembered.rand; end
|
||||
def random_element; @@remembered.random_element; end
|
||||
end
|
||||
end
|
||||
|
||||
|
@ -81,14 +82,14 @@ class EagerLoadPolyAssocsTest < ActiveRecord::TestCase
|
|||
[Circle, Square, Triangle, NonPolyOne, NonPolyTwo].map(&:create!)
|
||||
end
|
||||
1.upto(NUM_SIMPLE_OBJS) do
|
||||
PaintColor.create!(:non_poly_one_id => NonPolyOne.rand.id)
|
||||
PaintTexture.create!(:non_poly_two_id => NonPolyTwo.rand.id)
|
||||
PaintColor.create!(:non_poly_one_id => NonPolyOne.random_element.id)
|
||||
PaintTexture.create!(:non_poly_two_id => NonPolyTwo.random_element.id)
|
||||
end
|
||||
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)
|
||||
shape_type = [Circle, Square, Triangle].random_element
|
||||
paint_type = [PaintColor, PaintTexture].random_element
|
||||
ShapeExpression.create!(:shape_type => shape_type.to_s, :shape_id => shape_type.random_element.id,
|
||||
:paint_type => paint_type.to_s, :paint_id => paint_type.random_element.id)
|
||||
end
|
||||
end
|
||||
|
||||
|
|
|
@ -830,5 +830,11 @@ class EagerAssociationTest < ActiveRecord::TestCase
|
|||
assert_equal expected, firm.account_using_primary_key
|
||||
end
|
||||
end
|
||||
|
||||
|
||||
def test_preloading_empty_polymorphic_parent
|
||||
t = Tagging.create!(:taggable_type => 'Post', :taggable_id => Post.maximum(:id) + 1, :tag => tags(:general))
|
||||
|
||||
assert_queries(2) { @tagging = Tagging.find(t.id, :include => :taggable) }
|
||||
assert_no_queries { assert ! @tagging.taggable }
|
||||
end
|
||||
end
|
||||
|
|
|
@ -1130,5 +1130,31 @@ class HasManyAssociationsTest < ActiveRecord::TestCase
|
|||
client = firm.clients_using_primary_key.create!(:name => 'test')
|
||||
assert_equal firm.name, client.firm_name
|
||||
end
|
||||
|
||||
def test_normal_method_call_in_association_proxy
|
||||
assert_equal 'Welcome to the weblog', Comment.all.map { |comment| comment.post }.sort_by(&:id).first.title
|
||||
end
|
||||
|
||||
def test_instance_eval_in_association_proxy
|
||||
assert_equal 'Welcome to the weblog', Comment.all.map { |comment| comment.post }.sort_by(&:id).first.instance_eval{title}
|
||||
end
|
||||
|
||||
def test_defining_has_many_association_with_delete_all_dependency_lazily_evaluates_target_class
|
||||
ActiveRecord::Reflection::AssociationReflection.any_instance.expects(:class_name).never
|
||||
class_eval <<-EOF
|
||||
class DeleteAllModel < ActiveRecord::Base
|
||||
has_many :nonentities, :dependent => :delete_all
|
||||
end
|
||||
EOF
|
||||
end
|
||||
|
||||
def test_defining_has_many_association_with_nullify_dependency_lazily_evaluates_target_class
|
||||
ActiveRecord::Reflection::AssociationReflection.any_instance.expects(:class_name).never
|
||||
class_eval <<-EOF
|
||||
class NullifyModel < ActiveRecord::Base
|
||||
has_many :nonentities, :dependent => :nullify
|
||||
end
|
||||
EOF
|
||||
end
|
||||
end
|
||||
|
||||
|
|
566
vendor/rails/activerecord/test/cases/associations/inverse_associations_test.rb
vendored
Normal file
566
vendor/rails/activerecord/test/cases/associations/inverse_associations_test.rb
vendored
Normal file
|
@ -0,0 +1,566 @@
|
|||
require "cases/helper"
|
||||
require 'models/man'
|
||||
require 'models/face'
|
||||
require 'models/interest'
|
||||
require 'models/zine'
|
||||
require 'models/club'
|
||||
require 'models/sponsor'
|
||||
|
||||
class InverseAssociationTests < ActiveRecord::TestCase
|
||||
def test_should_allow_for_inverse_of_options_in_associations
|
||||
assert_nothing_raised(ArgumentError, 'ActiveRecord should allow the inverse_of options on has_many') do
|
||||
Class.new(ActiveRecord::Base).has_many(:wheels, :inverse_of => :car)
|
||||
end
|
||||
|
||||
assert_nothing_raised(ArgumentError, 'ActiveRecord should allow the inverse_of options on has_one') do
|
||||
Class.new(ActiveRecord::Base).has_one(:engine, :inverse_of => :car)
|
||||
end
|
||||
|
||||
assert_nothing_raised(ArgumentError, 'ActiveRecord should allow the inverse_of options on belongs_to') do
|
||||
Class.new(ActiveRecord::Base).belongs_to(:car, :inverse_of => :driver)
|
||||
end
|
||||
end
|
||||
|
||||
def test_should_be_able_to_ask_a_reflection_if_it_has_an_inverse
|
||||
has_one_with_inverse_ref = Man.reflect_on_association(:face)
|
||||
assert has_one_with_inverse_ref.respond_to?(:has_inverse?)
|
||||
assert has_one_with_inverse_ref.has_inverse?
|
||||
|
||||
has_many_with_inverse_ref = Man.reflect_on_association(:interests)
|
||||
assert has_many_with_inverse_ref.respond_to?(:has_inverse?)
|
||||
assert has_many_with_inverse_ref.has_inverse?
|
||||
|
||||
belongs_to_with_inverse_ref = Face.reflect_on_association(:man)
|
||||
assert belongs_to_with_inverse_ref.respond_to?(:has_inverse?)
|
||||
assert belongs_to_with_inverse_ref.has_inverse?
|
||||
|
||||
has_one_without_inverse_ref = Club.reflect_on_association(:sponsor)
|
||||
assert has_one_without_inverse_ref.respond_to?(:has_inverse?)
|
||||
assert !has_one_without_inverse_ref.has_inverse?
|
||||
|
||||
has_many_without_inverse_ref = Club.reflect_on_association(:memberships)
|
||||
assert has_many_without_inverse_ref.respond_to?(:has_inverse?)
|
||||
assert !has_many_without_inverse_ref.has_inverse?
|
||||
|
||||
belongs_to_without_inverse_ref = Sponsor.reflect_on_association(:sponsor_club)
|
||||
assert belongs_to_without_inverse_ref.respond_to?(:has_inverse?)
|
||||
assert !belongs_to_without_inverse_ref.has_inverse?
|
||||
end
|
||||
|
||||
def test_should_be_able_to_ask_a_reflection_what_it_is_the_inverse_of
|
||||
has_one_ref = Man.reflect_on_association(:face)
|
||||
assert has_one_ref.respond_to?(:inverse_of)
|
||||
|
||||
has_many_ref = Man.reflect_on_association(:interests)
|
||||
assert has_many_ref.respond_to?(:inverse_of)
|
||||
|
||||
belongs_to_ref = Face.reflect_on_association(:man)
|
||||
assert belongs_to_ref.respond_to?(:inverse_of)
|
||||
end
|
||||
|
||||
def test_inverse_of_method_should_supply_the_actual_reflection_instance_it_is_the_inverse_of
|
||||
has_one_ref = Man.reflect_on_association(:face)
|
||||
assert_equal Face.reflect_on_association(:man), has_one_ref.inverse_of
|
||||
|
||||
has_many_ref = Man.reflect_on_association(:interests)
|
||||
assert_equal Interest.reflect_on_association(:man), has_many_ref.inverse_of
|
||||
|
||||
belongs_to_ref = Face.reflect_on_association(:man)
|
||||
assert_equal Man.reflect_on_association(:face), belongs_to_ref.inverse_of
|
||||
end
|
||||
|
||||
def test_associations_with_no_inverse_of_should_return_nil
|
||||
has_one_ref = Club.reflect_on_association(:sponsor)
|
||||
assert_nil has_one_ref.inverse_of
|
||||
|
||||
has_many_ref = Club.reflect_on_association(:memberships)
|
||||
assert_nil has_many_ref.inverse_of
|
||||
|
||||
belongs_to_ref = Sponsor.reflect_on_association(:sponsor_club)
|
||||
assert_nil belongs_to_ref.inverse_of
|
||||
end
|
||||
end
|
||||
|
||||
class InverseHasOneTests < ActiveRecord::TestCase
|
||||
fixtures :men, :faces
|
||||
|
||||
def test_parent_instance_should_be_shared_with_child_on_find
|
||||
m = men(:gordon)
|
||||
f = m.face
|
||||
assert_equal m.name, f.man.name, "Name of man should be the same before changes to parent instance"
|
||||
m.name = 'Bongo'
|
||||
assert_equal m.name, f.man.name, "Name of man should be the same after changes to parent instance"
|
||||
f.man.name = 'Mungo'
|
||||
assert_equal m.name, f.man.name, "Name of man should be the same after changes to child-owned instance"
|
||||
end
|
||||
|
||||
def test_parent_instance_should_be_shared_with_eager_loaded_child_on_find
|
||||
m = Man.find(:first, :conditions => {:name => 'Gordon'}, :include => :face)
|
||||
f = m.face
|
||||
assert_equal m.name, f.man.name, "Name of man should be the same before changes to parent instance"
|
||||
m.name = 'Bongo'
|
||||
assert_equal m.name, f.man.name, "Name of man should be the same after changes to parent instance"
|
||||
f.man.name = 'Mungo'
|
||||
assert_equal m.name, f.man.name, "Name of man should be the same after changes to child-owned instance"
|
||||
|
||||
m = Man.find(:first, :conditions => {:name => 'Gordon'}, :include => :face, :order => 'faces.id')
|
||||
f = m.face
|
||||
assert_equal m.name, f.man.name, "Name of man should be the same before changes to parent instance"
|
||||
m.name = 'Bongo'
|
||||
assert_equal m.name, f.man.name, "Name of man should be the same after changes to parent instance"
|
||||
f.man.name = 'Mungo'
|
||||
assert_equal m.name, f.man.name, "Name of man should be the same after changes to child-owned instance"
|
||||
end
|
||||
|
||||
def test_parent_instance_should_be_shared_with_newly_built_child
|
||||
m = men(:gordon)
|
||||
f = m.build_face(:description => 'haunted')
|
||||
assert_not_nil f.man
|
||||
assert_equal m.name, f.man.name, "Name of man should be the same before changes to parent instance"
|
||||
m.name = 'Bongo'
|
||||
assert_equal m.name, f.man.name, "Name of man should be the same after changes to parent instance"
|
||||
f.man.name = 'Mungo'
|
||||
assert_equal m.name, f.man.name, "Name of man should be the same after changes to just-built-child-owned instance"
|
||||
end
|
||||
|
||||
def test_parent_instance_should_be_shared_with_newly_created_child
|
||||
m = men(:gordon)
|
||||
f = m.create_face(:description => 'haunted')
|
||||
assert_not_nil f.man
|
||||
assert_equal m.name, f.man.name, "Name of man should be the same before changes to parent instance"
|
||||
m.name = 'Bongo'
|
||||
assert_equal m.name, f.man.name, "Name of man should be the same after changes to parent instance"
|
||||
f.man.name = 'Mungo'
|
||||
assert_equal m.name, f.man.name, "Name of man should be the same after changes to newly-created-child-owned instance"
|
||||
end
|
||||
|
||||
def test_parent_instance_should_be_shared_with_newly_created_child_via_bang_method
|
||||
m = Man.find(:first)
|
||||
f = m.face.create!(:description => 'haunted')
|
||||
assert_not_nil f.man
|
||||
assert_equal m.name, f.man.name, "Name of man should be the same before changes to parent instance"
|
||||
m.name = 'Bongo'
|
||||
assert_equal m.name, f.man.name, "Name of man should be the same after changes to parent instance"
|
||||
f.man.name = 'Mungo'
|
||||
assert_equal m.name, f.man.name, "Name of man should be the same after changes to newly-created-child-owned instance"
|
||||
end
|
||||
|
||||
def test_parent_instance_should_be_shared_with_newly_built_child_when_we_dont_replace_existing
|
||||
m = Man.find(:first)
|
||||
f = m.build_face({:description => 'haunted'}, false)
|
||||
assert_not_nil f.man
|
||||
assert_equal m.name, f.man.name, "Name of man should be the same before changes to parent instance"
|
||||
m.name = 'Bongo'
|
||||
assert_equal m.name, f.man.name, "Name of man should be the same after changes to parent instance"
|
||||
f.man.name = 'Mungo'
|
||||
assert_equal m.name, f.man.name, "Name of man should be the same after changes to just-built-child-owned instance"
|
||||
end
|
||||
|
||||
def test_parent_instance_should_be_shared_with_newly_created_child_when_we_dont_replace_existing
|
||||
m = Man.find(:first)
|
||||
f = m.create_face({:description => 'haunted'}, false)
|
||||
assert_not_nil f.man
|
||||
assert_equal m.name, f.man.name, "Name of man should be the same before changes to parent instance"
|
||||
m.name = 'Bongo'
|
||||
assert_equal m.name, f.man.name, "Name of man should be the same after changes to parent instance"
|
||||
f.man.name = 'Mungo'
|
||||
assert_equal m.name, f.man.name, "Name of man should be the same after changes to newly-created-child-owned instance"
|
||||
end
|
||||
|
||||
def test_parent_instance_should_be_shared_with_newly_created_child_via_bang_method_when_we_dont_replace_existing
|
||||
m = Man.find(:first)
|
||||
f = m.face.create!({:description => 'haunted'}, false)
|
||||
assert_not_nil f.man
|
||||
assert_equal m.name, f.man.name, "Name of man should be the same before changes to parent instance"
|
||||
m.name = 'Bongo'
|
||||
assert_equal m.name, f.man.name, "Name of man should be the same after changes to parent instance"
|
||||
f.man.name = 'Mungo'
|
||||
assert_equal m.name, f.man.name, "Name of man should be the same after changes to newly-created-child-owned instance"
|
||||
end
|
||||
|
||||
def test_parent_instance_should_be_shared_with_replaced_via_accessor_child
|
||||
m = Man.find(:first)
|
||||
f = Face.new(:description => 'haunted')
|
||||
m.face = f
|
||||
assert_not_nil f.man
|
||||
assert_equal m.name, f.man.name, "Name of man should be the same before changes to parent instance"
|
||||
m.name = 'Bongo'
|
||||
assert_equal m.name, f.man.name, "Name of man should be the same after changes to parent instance"
|
||||
f.man.name = 'Mungo'
|
||||
assert_equal m.name, f.man.name, "Name of man should be the same after changes to replaced-child-owned instance"
|
||||
end
|
||||
|
||||
def test_parent_instance_should_be_shared_with_replaced_via_method_child
|
||||
m = Man.find(:first)
|
||||
f = Face.new(:description => 'haunted')
|
||||
m.face.replace(f)
|
||||
assert_not_nil f.man
|
||||
assert_equal m.name, f.man.name, "Name of man should be the same before changes to parent instance"
|
||||
m.name = 'Bongo'
|
||||
assert_equal m.name, f.man.name, "Name of man should be the same after changes to parent instance"
|
||||
f.man.name = 'Mungo'
|
||||
assert_equal m.name, f.man.name, "Name of man should be the same after changes to replaced-child-owned instance"
|
||||
end
|
||||
|
||||
def test_parent_instance_should_be_shared_with_replaced_via_method_child_when_we_dont_replace_existing
|
||||
m = Man.find(:first)
|
||||
f = Face.new(:description => 'haunted')
|
||||
m.face.replace(f, false)
|
||||
assert_not_nil f.man
|
||||
assert_equal m.name, f.man.name, "Name of man should be the same before changes to parent instance"
|
||||
m.name = 'Bongo'
|
||||
assert_equal m.name, f.man.name, "Name of man should be the same after changes to parent instance"
|
||||
f.man.name = 'Mungo'
|
||||
assert_equal m.name, f.man.name, "Name of man should be the same after changes to replaced-child-owned instance"
|
||||
end
|
||||
|
||||
def test_trying_to_use_inverses_that_dont_exist_should_raise_an_error
|
||||
assert_raise(ActiveRecord::InverseOfAssociationNotFoundError) { Man.find(:first).dirty_face }
|
||||
end
|
||||
end
|
||||
|
||||
class InverseHasManyTests < ActiveRecord::TestCase
|
||||
fixtures :men, :interests
|
||||
|
||||
def test_parent_instance_should_be_shared_with_every_child_on_find
|
||||
m = men(:gordon)
|
||||
is = m.interests
|
||||
is.each do |i|
|
||||
assert_equal m.name, i.man.name, "Name of man should be the same before changes to parent instance"
|
||||
m.name = 'Bongo'
|
||||
assert_equal m.name, i.man.name, "Name of man should be the same after changes to parent instance"
|
||||
i.man.name = 'Mungo'
|
||||
assert_equal m.name, i.man.name, "Name of man should be the same after changes to child-owned instance"
|
||||
end
|
||||
end
|
||||
|
||||
def test_parent_instance_should_be_shared_with_eager_loaded_children
|
||||
m = Man.find(:first, :conditions => {:name => 'Gordon'}, :include => :interests)
|
||||
is = m.interests
|
||||
is.each do |i|
|
||||
assert_equal m.name, i.man.name, "Name of man should be the same before changes to parent instance"
|
||||
m.name = 'Bongo'
|
||||
assert_equal m.name, i.man.name, "Name of man should be the same after changes to parent instance"
|
||||
i.man.name = 'Mungo'
|
||||
assert_equal m.name, i.man.name, "Name of man should be the same after changes to child-owned instance"
|
||||
end
|
||||
|
||||
m = Man.find(:first, :conditions => {:name => 'Gordon'}, :include => :interests, :order => 'interests.id')
|
||||
is = m.interests
|
||||
is.each do |i|
|
||||
assert_equal m.name, i.man.name, "Name of man should be the same before changes to parent instance"
|
||||
m.name = 'Bongo'
|
||||
assert_equal m.name, i.man.name, "Name of man should be the same after changes to parent instance"
|
||||
i.man.name = 'Mungo'
|
||||
assert_equal m.name, i.man.name, "Name of man should be the same after changes to child-owned instance"
|
||||
end
|
||||
end
|
||||
|
||||
def test_parent_instance_should_be_shared_with_newly_built_child
|
||||
m = men(:gordon)
|
||||
i = m.interests.build(:topic => 'Industrial Revolution Re-enactment')
|
||||
assert_not_nil i.man
|
||||
assert_equal m.name, i.man.name, "Name of man should be the same before changes to parent instance"
|
||||
m.name = 'Bongo'
|
||||
assert_equal m.name, i.man.name, "Name of man should be the same after changes to parent instance"
|
||||
i.man.name = 'Mungo'
|
||||
assert_equal m.name, i.man.name, "Name of man should be the same after changes to just-built-child-owned instance"
|
||||
end
|
||||
|
||||
def test_parent_instance_should_be_shared_with_newly_block_style_built_child
|
||||
m = Man.find(:first)
|
||||
i = m.interests.build {|ii| ii.topic = 'Industrial Revolution Re-enactment'}
|
||||
assert_not_nil i.topic, "Child attributes supplied to build via blocks should be populated"
|
||||
assert_not_nil i.man
|
||||
assert_equal m.name, i.man.name, "Name of man should be the same before changes to parent instance"
|
||||
m.name = 'Bongo'
|
||||
assert_equal m.name, i.man.name, "Name of man should be the same after changes to parent instance"
|
||||
i.man.name = 'Mungo'
|
||||
assert_equal m.name, i.man.name, "Name of man should be the same after changes to just-built-child-owned instance"
|
||||
end
|
||||
|
||||
def test_parent_instance_should_be_shared_with_newly_created_child
|
||||
m = men(:gordon)
|
||||
i = m.interests.create(:topic => 'Industrial Revolution Re-enactment')
|
||||
assert_not_nil i.man
|
||||
assert_equal m.name, i.man.name, "Name of man should be the same before changes to parent instance"
|
||||
m.name = 'Bongo'
|
||||
assert_equal m.name, i.man.name, "Name of man should be the same after changes to parent instance"
|
||||
i.man.name = 'Mungo'
|
||||
assert_equal m.name, i.man.name, "Name of man should be the same after changes to newly-created-child-owned instance"
|
||||
end
|
||||
|
||||
def test_parent_instance_should_be_shared_with_newly_created_via_bang_method_child
|
||||
m = Man.find(:first)
|
||||
i = m.interests.create!(:topic => 'Industrial Revolution Re-enactment')
|
||||
assert_not_nil i.man
|
||||
assert_equal m.name, i.man.name, "Name of man should be the same before changes to parent instance"
|
||||
m.name = 'Bongo'
|
||||
assert_equal m.name, i.man.name, "Name of man should be the same after changes to parent instance"
|
||||
i.man.name = 'Mungo'
|
||||
assert_equal m.name, i.man.name, "Name of man should be the same after changes to newly-created-child-owned instance"
|
||||
end
|
||||
|
||||
def test_parent_instance_should_be_shared_with_newly_block_style_created_child
|
||||
m = Man.find(:first)
|
||||
i = m.interests.create {|ii| ii.topic = 'Industrial Revolution Re-enactment'}
|
||||
assert_not_nil i.topic, "Child attributes supplied to create via blocks should be populated"
|
||||
assert_not_nil i.man
|
||||
assert_equal m.name, i.man.name, "Name of man should be the same before changes to parent instance"
|
||||
m.name = 'Bongo'
|
||||
assert_equal m.name, i.man.name, "Name of man should be the same after changes to parent instance"
|
||||
i.man.name = 'Mungo'
|
||||
assert_equal m.name, i.man.name, "Name of man should be the same after changes to newly-created-child-owned instance"
|
||||
end
|
||||
|
||||
def test_parent_instance_should_be_shared_with_poked_in_child
|
||||
m = men(:gordon)
|
||||
i = Interest.create(:topic => 'Industrial Revolution Re-enactment')
|
||||
m.interests << i
|
||||
assert_not_nil i.man
|
||||
assert_equal m.name, i.man.name, "Name of man should be the same before changes to parent instance"
|
||||
m.name = 'Bongo'
|
||||
assert_equal m.name, i.man.name, "Name of man should be the same after changes to parent instance"
|
||||
i.man.name = 'Mungo'
|
||||
assert_equal m.name, i.man.name, "Name of man should be the same after changes to newly-created-child-owned instance"
|
||||
end
|
||||
|
||||
def test_parent_instance_should_be_shared_with_replaced_via_accessor_children
|
||||
m = Man.find(:first)
|
||||
i = Interest.new(:topic => 'Industrial Revolution Re-enactment')
|
||||
m.interests = [i]
|
||||
assert_not_nil i.man
|
||||
assert_equal m.name, i.man.name, "Name of man should be the same before changes to parent instance"
|
||||
m.name = 'Bongo'
|
||||
assert_equal m.name, i.man.name, "Name of man should be the same after changes to parent instance"
|
||||
i.man.name = 'Mungo'
|
||||
assert_equal m.name, i.man.name, "Name of man should be the same after changes to replaced-child-owned instance"
|
||||
end
|
||||
|
||||
def test_parent_instance_should_be_shared_with_replaced_via_method_children
|
||||
m = Man.find(:first)
|
||||
i = Interest.new(:topic => 'Industrial Revolution Re-enactment')
|
||||
m.interests.replace([i])
|
||||
assert_not_nil i.man
|
||||
assert_equal m.name, i.man.name, "Name of man should be the same before changes to parent instance"
|
||||
m.name = 'Bongo'
|
||||
assert_equal m.name, i.man.name, "Name of man should be the same after changes to parent instance"
|
||||
i.man.name = 'Mungo'
|
||||
assert_equal m.name, i.man.name, "Name of man should be the same after changes to replaced-child-owned instance"
|
||||
end
|
||||
|
||||
def test_trying_to_use_inverses_that_dont_exist_should_raise_an_error
|
||||
assert_raise(ActiveRecord::InverseOfAssociationNotFoundError) { Man.find(:first).secret_interests }
|
||||
end
|
||||
end
|
||||
|
||||
class InverseBelongsToTests < ActiveRecord::TestCase
|
||||
fixtures :men, :faces, :interests
|
||||
|
||||
def test_child_instance_should_be_shared_with_parent_on_find
|
||||
f = faces(:trusting)
|
||||
m = f.man
|
||||
assert_equal f.description, m.face.description, "Description of face should be the same before changes to child instance"
|
||||
f.description = 'gormless'
|
||||
assert_equal f.description, m.face.description, "Description of face should be the same after changes to child instance"
|
||||
m.face.description = 'pleasing'
|
||||
assert_equal f.description, m.face.description, "Description of face should be the same after changes to parent-owned instance"
|
||||
end
|
||||
|
||||
def test_eager_loaded_child_instance_should_be_shared_with_parent_on_find
|
||||
f = Face.find(:first, :include => :man, :conditions => {:description => 'trusting'})
|
||||
m = f.man
|
||||
assert_equal f.description, m.face.description, "Description of face should be the same before changes to child instance"
|
||||
f.description = 'gormless'
|
||||
assert_equal f.description, m.face.description, "Description of face should be the same after changes to child instance"
|
||||
m.face.description = 'pleasing'
|
||||
assert_equal f.description, m.face.description, "Description of face should be the same after changes to parent-owned instance"
|
||||
|
||||
f = Face.find(:first, :include => :man, :order => 'men.id', :conditions => {:description => 'trusting'})
|
||||
m = f.man
|
||||
assert_equal f.description, m.face.description, "Description of face should be the same before changes to child instance"
|
||||
f.description = 'gormless'
|
||||
assert_equal f.description, m.face.description, "Description of face should be the same after changes to child instance"
|
||||
m.face.description = 'pleasing'
|
||||
assert_equal f.description, m.face.description, "Description of face should be the same after changes to parent-owned instance"
|
||||
end
|
||||
|
||||
def test_child_instance_should_be_shared_with_newly_built_parent
|
||||
f = faces(:trusting)
|
||||
m = f.build_man(:name => 'Charles')
|
||||
assert_not_nil m.face
|
||||
assert_equal f.description, m.face.description, "Description of face should be the same before changes to child instance"
|
||||
f.description = 'gormless'
|
||||
assert_equal f.description, m.face.description, "Description of face should be the same after changes to child instance"
|
||||
m.face.description = 'pleasing'
|
||||
assert_equal f.description, m.face.description, "Description of face should be the same after changes to just-built-parent-owned instance"
|
||||
end
|
||||
|
||||
def test_child_instance_should_be_shared_with_newly_created_parent
|
||||
f = faces(:trusting)
|
||||
m = f.create_man(:name => 'Charles')
|
||||
assert_not_nil m.face
|
||||
assert_equal f.description, m.face.description, "Description of face should be the same before changes to child instance"
|
||||
f.description = 'gormless'
|
||||
assert_equal f.description, m.face.description, "Description of face should be the same after changes to child instance"
|
||||
m.face.description = 'pleasing'
|
||||
assert_equal f.description, m.face.description, "Description of face should be the same after changes to newly-created-parent-owned instance"
|
||||
end
|
||||
|
||||
def test_should_not_try_to_set_inverse_instances_when_the_inverse_is_a_has_many
|
||||
i = interests(:trainspotting)
|
||||
m = i.man
|
||||
assert_not_nil m.interests
|
||||
iz = m.interests.detect {|iz| iz.id == i.id}
|
||||
assert_not_nil iz
|
||||
assert_equal i.topic, iz.topic, "Interest topics should be the same before changes to child"
|
||||
i.topic = 'Eating cheese with a spoon'
|
||||
assert_not_equal i.topic, iz.topic, "Interest topics should not be the same after changes to child"
|
||||
iz.topic = 'Cow tipping'
|
||||
assert_not_equal i.topic, iz.topic, "Interest topics should not be the same after changes to parent-owned instance"
|
||||
end
|
||||
|
||||
def test_child_instance_should_be_shared_with_replaced_via_accessor_parent
|
||||
f = Face.find(:first)
|
||||
m = Man.new(:name => 'Charles')
|
||||
f.man = m
|
||||
assert_not_nil m.face
|
||||
assert_equal f.description, m.face.description, "Description of face should be the same before changes to child instance"
|
||||
f.description = 'gormless'
|
||||
assert_equal f.description, m.face.description, "Description of face should be the same after changes to child instance"
|
||||
m.face.description = 'pleasing'
|
||||
assert_equal f.description, m.face.description, "Description of face should be the same after changes to replaced-parent-owned instance"
|
||||
end
|
||||
|
||||
def test_child_instance_should_be_shared_with_replaced_via_method_parent
|
||||
f = faces(:trusting)
|
||||
assert_not_nil f.man
|
||||
m = Man.new(:name => 'Charles')
|
||||
f.man.replace(m)
|
||||
assert_not_nil m.face
|
||||
assert_equal f.description, m.face.description, "Description of face should be the same before changes to child instance"
|
||||
f.description = 'gormless'
|
||||
assert_equal f.description, m.face.description, "Description of face should be the same after changes to child instance"
|
||||
m.face.description = 'pleasing'
|
||||
assert_equal f.description, m.face.description, "Description of face should be the same after changes to replaced-parent-owned instance"
|
||||
end
|
||||
|
||||
def test_trying_to_use_inverses_that_dont_exist_should_raise_an_error
|
||||
assert_raise(ActiveRecord::InverseOfAssociationNotFoundError) { Face.find(:first).horrible_man }
|
||||
end
|
||||
end
|
||||
|
||||
class InversePolymorphicBelongsToTests < ActiveRecord::TestCase
|
||||
fixtures :men, :faces, :interests
|
||||
|
||||
def test_child_instance_should_be_shared_with_parent_on_find
|
||||
f = Face.find(:first, :conditions => {:description => 'confused'})
|
||||
m = f.polymorphic_man
|
||||
assert_equal f.description, m.polymorphic_face.description, "Description of face should be the same before changes to child instance"
|
||||
f.description = 'gormless'
|
||||
assert_equal f.description, m.polymorphic_face.description, "Description of face should be the same after changes to child instance"
|
||||
m.polymorphic_face.description = 'pleasing'
|
||||
assert_equal f.description, m.polymorphic_face.description, "Description of face should be the same after changes to parent-owned instance"
|
||||
end
|
||||
|
||||
def test_eager_loaded_child_instance_should_be_shared_with_parent_on_find
|
||||
f = Face.find(:first, :conditions => {:description => 'confused'}, :include => :man)
|
||||
m = f.polymorphic_man
|
||||
assert_equal f.description, m.polymorphic_face.description, "Description of face should be the same before changes to child instance"
|
||||
f.description = 'gormless'
|
||||
assert_equal f.description, m.polymorphic_face.description, "Description of face should be the same after changes to child instance"
|
||||
m.polymorphic_face.description = 'pleasing'
|
||||
assert_equal f.description, m.polymorphic_face.description, "Description of face should be the same after changes to parent-owned instance"
|
||||
|
||||
f = Face.find(:first, :conditions => {:description => 'confused'}, :include => :man, :order => 'men.id')
|
||||
m = f.polymorphic_man
|
||||
assert_equal f.description, m.polymorphic_face.description, "Description of face should be the same before changes to child instance"
|
||||
f.description = 'gormless'
|
||||
assert_equal f.description, m.polymorphic_face.description, "Description of face should be the same after changes to child instance"
|
||||
m.polymorphic_face.description = 'pleasing'
|
||||
assert_equal f.description, m.polymorphic_face.description, "Description of face should be the same after changes to parent-owned instance"
|
||||
end
|
||||
|
||||
def test_child_instance_should_be_shared_with_replaced_via_accessor_parent
|
||||
face = faces(:confused)
|
||||
old_man = face.polymorphic_man
|
||||
new_man = Man.new
|
||||
|
||||
assert_not_nil face.polymorphic_man
|
||||
face.polymorphic_man = new_man
|
||||
|
||||
assert_equal face.description, new_man.polymorphic_face.description, "Description of face should be the same before changes to parent instance"
|
||||
face.description = 'Bongo'
|
||||
assert_equal face.description, new_man.polymorphic_face.description, "Description of face should be the same after changes to parent instance"
|
||||
new_man.polymorphic_face.description = 'Mungo'
|
||||
assert_equal face.description, new_man.polymorphic_face.description, "Description of face should be the same after changes to replaced-parent-owned instance"
|
||||
end
|
||||
|
||||
def test_child_instance_should_be_shared_with_replaced_via_method_parent
|
||||
face = faces(:confused)
|
||||
old_man = face.polymorphic_man
|
||||
new_man = Man.new
|
||||
|
||||
assert_not_nil face.polymorphic_man
|
||||
face.polymorphic_man.replace(new_man)
|
||||
|
||||
assert_equal face.description, new_man.polymorphic_face.description, "Description of face should be the same before changes to parent instance"
|
||||
face.description = 'Bongo'
|
||||
assert_equal face.description, new_man.polymorphic_face.description, "Description of face should be the same after changes to parent instance"
|
||||
new_man.polymorphic_face.description = 'Mungo'
|
||||
assert_equal face.description, new_man.polymorphic_face.description, "Description of face should be the same after changes to replaced-parent-owned instance"
|
||||
end
|
||||
|
||||
def test_should_not_try_to_set_inverse_instances_when_the_inverse_is_a_has_many
|
||||
i = interests(:llama_wrangling)
|
||||
m = i.polymorphic_man
|
||||
assert_not_nil m.polymorphic_interests
|
||||
iz = m.polymorphic_interests.detect {|iz| iz.id == i.id}
|
||||
assert_not_nil iz
|
||||
assert_equal i.topic, iz.topic, "Interest topics should be the same before changes to child"
|
||||
i.topic = 'Eating cheese with a spoon'
|
||||
assert_not_equal i.topic, iz.topic, "Interest topics should not be the same after changes to child"
|
||||
iz.topic = 'Cow tipping'
|
||||
assert_not_equal i.topic, iz.topic, "Interest topics should not be the same after changes to parent-owned instance"
|
||||
end
|
||||
|
||||
def test_trying_to_access_inverses_that_dont_exist_shouldnt_raise_an_error
|
||||
# Ideally this would, if only for symmetry's sake with other association types
|
||||
assert_nothing_raised(ActiveRecord::InverseOfAssociationNotFoundError) { Face.find(:first).horrible_polymorphic_man }
|
||||
end
|
||||
|
||||
def test_trying_to_set_polymorphic_inverses_that_dont_exist_at_all_should_raise_an_error
|
||||
# fails because no class has the correct inverse_of for horrible_polymorphic_man
|
||||
assert_raise(ActiveRecord::InverseOfAssociationNotFoundError) { Face.find(:first).horrible_polymorphic_man = Man.first }
|
||||
end
|
||||
|
||||
def test_trying_to_set_polymorphic_inverses_that_dont_exist_on_the_instance_being_set_should_raise_an_error
|
||||
# passes because Man does have the correct inverse_of
|
||||
assert_nothing_raised(ActiveRecord::InverseOfAssociationNotFoundError) { Face.find(:first).polymorphic_man = Man.first }
|
||||
# fails because Interest does have the correct inverse_of
|
||||
assert_raise(ActiveRecord::InverseOfAssociationNotFoundError) { Face.find(:first).polymorphic_man = Interest.first }
|
||||
end
|
||||
end
|
||||
|
||||
# NOTE - these tests might not be meaningful, ripped as they were from the parental_control plugin
|
||||
# which would guess the inverse rather than look for an explicit configuration option.
|
||||
class InverseMultipleHasManyInversesForSameModel < ActiveRecord::TestCase
|
||||
fixtures :men, :interests, :zines
|
||||
|
||||
def test_that_we_can_load_associations_that_have_the_same_reciprocal_name_from_different_models
|
||||
assert_nothing_raised(ActiveRecord::AssociationTypeMismatch) do
|
||||
i = Interest.find(:first)
|
||||
z = i.zine
|
||||
m = i.man
|
||||
end
|
||||
end
|
||||
|
||||
def test_that_we_can_create_associations_that_have_the_same_reciprocal_name_from_different_models
|
||||
assert_nothing_raised(ActiveRecord::AssociationTypeMismatch) do
|
||||
i = Interest.find(:first)
|
||||
i.build_zine(:title => 'Get Some in Winter! 2008')
|
||||
i.build_man(:name => 'Gordon')
|
||||
i.save!
|
||||
end
|
||||
end
|
||||
end
|
|
@ -64,6 +64,16 @@ class AssociationsTest < ActiveRecord::TestCase
|
|||
assert !firm.clients(true).empty?, "New firm should have reloaded client objects"
|
||||
assert_equal 1, firm.clients(true).size, "New firm should have reloaded clients count"
|
||||
end
|
||||
|
||||
def test_force_reload_is_uncached
|
||||
firm = Firm.create!("name" => "A New Firm, Inc")
|
||||
client = Client.create!("name" => "TheClient.com", :firm => firm)
|
||||
ActiveRecord::Base.cache do
|
||||
firm.clients.each {}
|
||||
assert_queries(0) { assert_not_nil firm.clients.each {} }
|
||||
assert_queries(1) { assert_not_nil firm.clients(true).each {} }
|
||||
end
|
||||
end
|
||||
|
||||
def test_storing_in_pstore
|
||||
require "tmpdir"
|
||||
|
|
|
@ -3,6 +3,8 @@ require 'models/bird'
|
|||
require 'models/company'
|
||||
require 'models/customer'
|
||||
require 'models/developer'
|
||||
require 'models/invoice'
|
||||
require 'models/line_item'
|
||||
require 'models/order'
|
||||
require 'models/parrot'
|
||||
require 'models/person'
|
||||
|
@ -30,11 +32,40 @@ class TestAutosaveAssociationsInGeneral < ActiveRecord::TestCase
|
|||
assert base.valid_keys_for_has_and_belongs_to_many_association.include?(:autosave)
|
||||
end
|
||||
|
||||
def test_should_not_add_the_same_callbacks_multiple_times_for_has_one
|
||||
assert_no_difference_when_adding_callbacks_twice_for Pirate, :ship
|
||||
end
|
||||
|
||||
def test_should_not_add_the_same_callbacks_multiple_times_for_belongs_to
|
||||
assert_no_difference_when_adding_callbacks_twice_for Ship, :pirate
|
||||
end
|
||||
|
||||
def test_should_not_add_the_same_callbacks_multiple_times_for_has_many
|
||||
assert_no_difference_when_adding_callbacks_twice_for Pirate, :birds
|
||||
end
|
||||
|
||||
def test_should_not_add_the_same_callbacks_multiple_times_for_has_and_belongs_to_many
|
||||
assert_no_difference_when_adding_callbacks_twice_for Pirate, :parrots
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
def base
|
||||
ActiveRecord::Base
|
||||
end
|
||||
|
||||
def assert_no_difference_when_adding_callbacks_twice_for(model, association_name)
|
||||
reflection = model.reflect_on_association(association_name)
|
||||
assert_no_difference "callbacks_for_model(#{model.name}).length" do
|
||||
model.send(:add_autosave_association_callbacks, reflection)
|
||||
end
|
||||
end
|
||||
|
||||
def callbacks_for_model(model)
|
||||
model.instance_variables.grep(/_callbacks$/).map do |ivar|
|
||||
model.instance_variable_get(ivar)
|
||||
end.flatten
|
||||
end
|
||||
end
|
||||
|
||||
class TestDefaultAutosaveAssociationOnAHasOneAssociation < ActiveRecord::TestCase
|
||||
|
@ -663,23 +694,18 @@ class TestDestroyAsPartOfAutosaveAssociation < ActiveRecord::TestCase
|
|||
|
||||
define_method("test_should_rollback_destructions_if_an_exception_occurred_while_saving_#{association_name}") do
|
||||
2.times { |i| @pirate.send(association_name).create!(:name => "#{association_name}_#{i}") }
|
||||
before = @pirate.send(association_name).map { |c| c }
|
||||
before = @pirate.send(association_name).map { |c| c.mark_for_destruction ; c }
|
||||
|
||||
# Stub the save method of the first child to destroy and the second to raise an exception
|
||||
class << before.first
|
||||
def save(*args)
|
||||
super
|
||||
destroy
|
||||
end
|
||||
end
|
||||
# Stub the destroy method of the the second child to raise an exception
|
||||
class << before.last
|
||||
def save(*args)
|
||||
def destroy(*args)
|
||||
super
|
||||
raise 'Oh noes!'
|
||||
end
|
||||
end
|
||||
|
||||
assert_raise(RuntimeError) { assert !@pirate.save }
|
||||
assert before.first.frozen? # the first child was indeed destroyed
|
||||
assert_equal before, @pirate.reload.send(association_name)
|
||||
end
|
||||
|
||||
|
@ -787,6 +813,18 @@ class TestAutosaveAssociationOnAHasOneAssociation < ActiveRecord::TestCase
|
|||
end
|
||||
end
|
||||
|
||||
def test_should_not_save_and_return_false_if_a_callback_cancelled_saving
|
||||
pirate = Pirate.new(:catchphrase => 'Arr')
|
||||
ship = pirate.build_ship(:name => 'The Vile Insanity')
|
||||
ship.cancel_save_from_callback = true
|
||||
|
||||
assert_no_difference 'Pirate.count' do
|
||||
assert_no_difference 'Ship.count' do
|
||||
assert !pirate.save
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
def test_should_rollback_any_changes_if_an_exception_occurred_while_saving
|
||||
before = [@pirate.catchphrase, @pirate.ship.name]
|
||||
|
||||
|
@ -865,6 +903,18 @@ class TestAutosaveAssociationOnABelongsToAssociation < ActiveRecord::TestCase
|
|||
end
|
||||
end
|
||||
|
||||
def test_should_not_save_and_return_false_if_a_callback_cancelled_saving
|
||||
ship = Ship.new(:name => 'The Vile Insanity')
|
||||
pirate = ship.build_pirate(:catchphrase => 'Arr')
|
||||
pirate.cancel_save_from_callback = true
|
||||
|
||||
assert_no_difference 'Ship.count' do
|
||||
assert_no_difference 'Pirate.count' do
|
||||
assert !ship.save
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
def test_should_rollback_any_changes_if_an_exception_occurred_while_saving
|
||||
before = [@ship.pirate.catchphrase, @ship.name]
|
||||
|
||||
|
@ -880,7 +930,6 @@ class TestAutosaveAssociationOnABelongsToAssociation < ActiveRecord::TestCase
|
|||
end
|
||||
|
||||
assert_raise(RuntimeError) { assert !@ship.save }
|
||||
# TODO: Why does using reload on @ship looses the associated pirate?
|
||||
assert_equal before, [@ship.pirate.reload.catchphrase, @ship.reload.name]
|
||||
end
|
||||
|
||||
|
@ -957,6 +1006,26 @@ module AutosaveAssociationOnACollectionAssociationTests
|
|||
end
|
||||
end
|
||||
|
||||
def test_should_not_save_and_return_false_if_a_callback_cancelled_saving_in_either_create_or_update
|
||||
@pirate.catchphrase = 'Changed'
|
||||
@child_1.name = 'Changed'
|
||||
@child_1.cancel_save_from_callback = true
|
||||
|
||||
assert !@pirate.save
|
||||
assert_equal "Don' botharrr talkin' like one, savvy?", @pirate.reload.catchphrase
|
||||
assert_equal "Posideons Killer", @child_1.reload.name
|
||||
|
||||
new_pirate = Pirate.new(:catchphrase => 'Arr')
|
||||
new_child = new_pirate.send(@association_name).build(:name => 'Grace OMalley')
|
||||
new_child.cancel_save_from_callback = true
|
||||
|
||||
assert_no_difference 'Pirate.count' do
|
||||
assert_no_difference "#{new_child.class.name}.count" do
|
||||
assert !new_pirate.save
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
def test_should_rollback_any_changes_if_an_exception_occurred_while_saving
|
||||
before = [@pirate.catchphrase, *@pirate.send(@association_name).map(&:name)]
|
||||
new_names = ['Grace OMalley', 'Privateers Greed']
|
||||
|
@ -1140,3 +1209,10 @@ class TestAutosaveAssociationValidationMethodsGeneration < ActiveRecord::TestCas
|
|||
assert !@pirate.respond_to?(:validate_associated_records_for_non_validated_parrots)
|
||||
end
|
||||
end
|
||||
|
||||
class TestAutosaveAssociationWithTouch < ActiveRecord::TestCase
|
||||
def test_autosave_with_touch_should_not_raise_system_stack_error
|
||||
invoice = Invoice.create
|
||||
assert_nothing_raised { invoice.line_items.create(:amount => 10) }
|
||||
end
|
||||
end
|
||||
|
|
|
@ -1,6 +1,7 @@
|
|||
require "cases/helper"
|
||||
require 'models/post'
|
||||
require 'models/author'
|
||||
require 'models/event_author'
|
||||
require 'models/topic'
|
||||
require 'models/reply'
|
||||
require 'models/category'
|
||||
|
@ -627,6 +628,16 @@ class BasicsTest < ActiveRecord::TestCase
|
|||
assert_equal -2, Topic.find(2).replies_count
|
||||
end
|
||||
|
||||
def test_reset_counters
|
||||
assert_equal 1, Topic.find(1).replies_count
|
||||
|
||||
Topic.increment_counter("replies_count", 1)
|
||||
assert_equal 2, Topic.find(1).replies_count
|
||||
|
||||
Topic.reset_counters(1, :replies)
|
||||
assert_equal 1, Topic.find(1).replies_count
|
||||
end
|
||||
|
||||
def test_update_counter
|
||||
category = categories(:general)
|
||||
assert_nil category.categorizations_count
|
||||
|
@ -1005,6 +1016,18 @@ class BasicsTest < ActiveRecord::TestCase
|
|||
assert_equal "changed", post.body
|
||||
end
|
||||
|
||||
def test_multiparameter_attribute_assignment_via_association_proxy
|
||||
multiparameter_date_attribute = {
|
||||
"ends_on(1i)" => "2004", "ends_on(2i)" => "6", "ends_on(3i)" => "24",
|
||||
"ends_on(4i)" => "16", "ends_on(5i)" => "24", "ends_on(6i)" => "00"
|
||||
}
|
||||
|
||||
author = Author.create(:name => "dhh")
|
||||
event = author.events.create(multiparameter_date_attribute)
|
||||
|
||||
assert_equal Time.local(2004,6,24,16,24,0),event.ends_on
|
||||
end
|
||||
|
||||
def test_multiparameter_attributes_on_date
|
||||
attributes = { "last_read(1i)" => "2004", "last_read(2i)" => "6", "last_read(3i)" => "24" }
|
||||
topic = Topic.find(1)
|
||||
|
@ -1622,6 +1645,12 @@ class BasicsTest < ActiveRecord::TestCase
|
|||
assert_equal t1.title, t2.title
|
||||
end
|
||||
|
||||
def test_reload_with_exclusive_scope
|
||||
dev = DeveloperCalledDavid.first
|
||||
dev.update_attributes!( :name => "NotDavid" )
|
||||
assert_equal dev, dev.reload
|
||||
end
|
||||
|
||||
def test_define_attr_method_with_value
|
||||
k = Class.new( ActiveRecord::Base )
|
||||
k.send(:define_attr_method, :table_name, "foo")
|
||||
|
|
|
@ -58,4 +58,24 @@ class EachTest < ActiveRecord::TestCase
|
|||
Post.find_in_batches(:batch_size => post_count + 1) {|batch| assert_kind_of Array, batch }
|
||||
end
|
||||
end
|
||||
|
||||
def test_find_in_batches_doesnt_clog_conditions
|
||||
Post.find_in_batches(:conditions => {:id => posts(:welcome).id}) do
|
||||
assert_nothing_raised { Post.find(posts(:thinking).id) }
|
||||
end
|
||||
end
|
||||
|
||||
def test_each_should_raise_if_select_is_set_without_id
|
||||
assert_raise(RuntimeError) do
|
||||
Post.find_each(:select => :title, :batch_size => 1) { |post| post }
|
||||
end
|
||||
end
|
||||
|
||||
def test_each_should_execute_if_id_is_in_select
|
||||
assert_queries(4) do
|
||||
Post.find_each(:select => "id, title, type", :batch_size => 2) do |post|
|
||||
assert_kind_of Post, post
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
|
@ -23,8 +23,7 @@ class CalculationsTest < ActiveRecord::TestCase
|
|||
|
||||
def test_should_average_field
|
||||
value = Account.average(:credit_limit)
|
||||
assert_kind_of BigDecimal, value
|
||||
assert_equal BigDecimal.new('53.0'), value
|
||||
assert_equal 53.0, value
|
||||
end
|
||||
|
||||
def test_should_return_nil_as_average
|
||||
|
@ -298,7 +297,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").to_i
|
||||
end
|
||||
|
||||
def test_count_with_from_option
|
||||
|
|
|
@ -518,8 +518,8 @@ class FinderTest < ActiveRecord::TestCase
|
|||
end
|
||||
|
||||
def test_string_sanitation
|
||||
assert_not_equal "#{ActiveRecord::Base.connection.quoted_string_prefix}'something ' 1=1'", ActiveRecord::Base.sanitize("something ' 1=1")
|
||||
assert_equal "#{ActiveRecord::Base.connection.quoted_string_prefix}'something; select table'", ActiveRecord::Base.sanitize("something; select table")
|
||||
assert_not_equal "'something ' 1=1'", ActiveRecord::Base.sanitize("something ' 1=1")
|
||||
assert_equal "'something; select table'", ActiveRecord::Base.sanitize("something; select table")
|
||||
end
|
||||
|
||||
def test_count
|
||||
|
@ -838,7 +838,7 @@ class FinderTest < ActiveRecord::TestCase
|
|||
assert c.new_record?
|
||||
end
|
||||
|
||||
def test_find_or_create_from_one_attribute_should_set_not_attribute_even_when_protected
|
||||
def test_find_or_create_from_one_attribute_should_not_set_attribute_even_when_protected
|
||||
c = Company.find_or_create_by_name({:name => "Fortune 1000", :rating => 1000})
|
||||
assert_equal "Fortune 1000", c.name
|
||||
assert_not_equal 1000, c.rating
|
||||
|
@ -846,6 +846,22 @@ class FinderTest < ActiveRecord::TestCase
|
|||
assert !c.new_record?
|
||||
end
|
||||
|
||||
def test_find_or_initialize_from_one_attribute_should_set_attribute_even_when_protected_and_also_set_the_hash
|
||||
c = Company.find_or_initialize_by_rating(1000, {:name => "Fortune 1000"})
|
||||
assert_equal "Fortune 1000", c.name
|
||||
assert_equal 1000, c.rating
|
||||
assert c.valid?
|
||||
assert c.new_record?
|
||||
end
|
||||
|
||||
def test_find_or_create_from_one_attribute_should_set_attribute_even_when_protected_and_also_set_the_hash
|
||||
c = Company.find_or_create_by_rating(1000, {:name => "Fortune 1000"})
|
||||
assert_equal "Fortune 1000", c.name
|
||||
assert_equal 1000, c.rating
|
||||
assert c.valid?
|
||||
assert !c.new_record?
|
||||
end
|
||||
|
||||
def test_find_or_initialize_from_one_attribute_should_set_attribute_even_when_protected
|
||||
c = Company.find_or_initialize_by_name_and_rating("Fortune 1000", 1000)
|
||||
assert_equal "Fortune 1000", c.name
|
||||
|
|
|
@ -254,6 +254,11 @@ class FixturesWithoutInstantiationTest < ActiveRecord::TestCase
|
|||
assert_nil @unknown
|
||||
end
|
||||
|
||||
def test_visibility_of_accessor_method
|
||||
assert_equal false, respond_to?(:topics, false), "should be private method"
|
||||
assert_equal true, respond_to?(:topics, true), "confirm to respond surely"
|
||||
end
|
||||
|
||||
def test_accessor_methods
|
||||
assert_equal "The First Topic", topics(:first).title
|
||||
assert_equal "Jamis", developers(:jamis).name
|
||||
|
|
|
@ -43,6 +43,20 @@ class JsonSerializationTest < ActiveRecord::TestCase
|
|||
Contact.include_root_in_json = false
|
||||
end
|
||||
|
||||
def test_should_include_root_in_json
|
||||
Contact.include_root_in_json = true
|
||||
json = @contact.to_json(:root => 'json_contact')
|
||||
|
||||
assert_match %r{^\{"json_contact":\{}, json
|
||||
assert_match %r{"name":"Konata Izumi"}, json
|
||||
assert_match %r{"age":16}, json
|
||||
assert json.include?(%("created_at":#{ActiveSupport::JSON.encode(Time.utc(2006, 8, 1))}))
|
||||
assert_match %r{"awesome":true}, json
|
||||
assert_match %r{"preferences":\{"shows":"anime"\}}, json
|
||||
ensure
|
||||
Contact.include_root_in_json = false
|
||||
end
|
||||
|
||||
def test_should_encode_all_encodable_attributes
|
||||
json = @contact.to_json
|
||||
|
||||
|
|
|
@ -38,6 +38,25 @@ class OptimisticLockingTest < ActiveRecord::TestCase
|
|||
assert_raise(ActiveRecord::StaleObjectError) { p2.save! }
|
||||
end
|
||||
|
||||
# See Lighthouse ticket #1966
|
||||
def test_lock_destroy
|
||||
p1 = Person.find(1)
|
||||
p2 = Person.find(1)
|
||||
assert_equal 0, p1.lock_version
|
||||
assert_equal 0, p2.lock_version
|
||||
|
||||
p1.first_name = 'stu'
|
||||
p1.save!
|
||||
assert_equal 1, p1.lock_version
|
||||
assert_equal 0, p2.lock_version
|
||||
|
||||
assert_raises(ActiveRecord::StaleObjectError) { p2.destroy }
|
||||
|
||||
assert p1.destroy
|
||||
assert_equal true, p1.frozen?
|
||||
assert_raises(ActiveRecord::RecordNotFound) { Person.find(1) }
|
||||
end
|
||||
|
||||
def test_lock_repeating
|
||||
p1 = Person.find(1)
|
||||
p2 = Person.find(1)
|
||||
|
@ -150,6 +169,32 @@ class OptimisticLockingTest < ActiveRecord::TestCase
|
|||
end
|
||||
end
|
||||
end
|
||||
|
||||
# See Lighthouse ticket #1966
|
||||
def test_destroy_dependents
|
||||
# Establish dependent relationship between People and LegacyThing
|
||||
add_counter_column_to(Person, 'legacy_things_count')
|
||||
LegacyThing.connection.add_column LegacyThing.table_name, 'person_id', :integer
|
||||
LegacyThing.reset_column_information
|
||||
LegacyThing.class_eval do
|
||||
belongs_to :person, :counter_cache => true
|
||||
end
|
||||
Person.class_eval do
|
||||
has_many :legacy_things, :dependent => :destroy
|
||||
end
|
||||
|
||||
# Make sure that counter incrementing doesn't cause problems
|
||||
p1 = Person.new(:first_name => 'fjord')
|
||||
p1.save!
|
||||
t = LegacyThing.new(:person => p1)
|
||||
t.save!
|
||||
p1.reload
|
||||
assert_equal 1, p1.legacy_things_count
|
||||
assert p1.destroy
|
||||
assert_equal true, p1.frozen?
|
||||
assert_raises(ActiveRecord::RecordNotFound) { Person.find(p1.id) }
|
||||
assert_raises(ActiveRecord::RecordNotFound) { LegacyThing.find(t.id) }
|
||||
end
|
||||
|
||||
def test_quote_table_name
|
||||
ref = references(:michael_magician)
|
||||
|
@ -168,11 +213,11 @@ class OptimisticLockingTest < ActiveRecord::TestCase
|
|||
|
||||
private
|
||||
|
||||
def add_counter_column_to(model)
|
||||
model.connection.add_column model.table_name, :test_count, :integer, :null => false, :default => 0
|
||||
def add_counter_column_to(model, col='test_count')
|
||||
model.connection.add_column model.table_name, col, :integer, :null => false, :default => 0
|
||||
model.reset_column_information
|
||||
# OpenBase does not set a value to existing rows when adding a not null default column
|
||||
model.update_all(:test_count => 0) if current_adapter?(:OpenBaseAdapter)
|
||||
model.update_all(col => 0) if current_adapter?(:OpenBaseAdapter)
|
||||
end
|
||||
|
||||
def remove_counter_column_from(model)
|
||||
|
|
|
@ -92,6 +92,14 @@ if ActiveRecord::Base.connection.supports_migrations?
|
|||
assert_nothing_raised { Person.connection.remove_index("people", "last_name_and_first_name") }
|
||||
assert_nothing_raised { Person.connection.add_index("people", ["last_name", "first_name"]) }
|
||||
assert_nothing_raised { Person.connection.remove_index("people", ["last_name", "first_name"]) }
|
||||
assert_nothing_raised { Person.connection.add_index("people", ["last_name"], :length => 10) }
|
||||
assert_nothing_raised { Person.connection.remove_index("people", "last_name") }
|
||||
assert_nothing_raised { Person.connection.add_index("people", ["last_name"], :length => {:last_name => 10}) }
|
||||
assert_nothing_raised { Person.connection.remove_index("people", ["last_name"]) }
|
||||
assert_nothing_raised { Person.connection.add_index("people", ["last_name", "first_name"], :length => 10) }
|
||||
assert_nothing_raised { Person.connection.remove_index("people", ["last_name", "first_name"]) }
|
||||
assert_nothing_raised { Person.connection.add_index("people", ["last_name", "first_name"], :length => {:last_name => 10, :first_name => 20}) }
|
||||
assert_nothing_raised { Person.connection.remove_index("people", ["last_name", "first_name"]) }
|
||||
end
|
||||
|
||||
# quoting
|
||||
|
@ -111,6 +119,40 @@ if ActiveRecord::Base.connection.supports_migrations?
|
|||
end
|
||||
end
|
||||
|
||||
def test_add_index_length_limit
|
||||
good_index_name = 'x' * Person.connection.index_name_length
|
||||
too_long_index_name = good_index_name + 'x'
|
||||
assert_nothing_raised { Person.connection.add_index("people", "first_name", :name => too_long_index_name) }
|
||||
assert !Person.connection.index_exists?("people", too_long_index_name, false)
|
||||
assert_nothing_raised { Person.connection.add_index("people", "first_name", :name => good_index_name) }
|
||||
assert Person.connection.index_exists?("people", good_index_name, false)
|
||||
end
|
||||
|
||||
def test_remove_nonexistent_index
|
||||
# we do this by name, so OpenBase is a wash as noted above
|
||||
unless current_adapter?(:OpenBaseAdapter)
|
||||
assert_nothing_raised { Person.connection.remove_index("people", "no_such_index") }
|
||||
end
|
||||
end
|
||||
|
||||
def test_rename_index
|
||||
unless current_adapter?(:OpenBaseAdapter)
|
||||
# keep the names short to make Oracle and similar behave
|
||||
Person.connection.add_index('people', [:first_name], :name => 'old_idx')
|
||||
assert_nothing_raised { Person.connection.rename_index('people', 'old_idx', 'new_idx') }
|
||||
# if the adapter doesn't support the indexes call, pick defaults that let the test pass
|
||||
assert !Person.connection.index_exists?('people', 'old_idx', false)
|
||||
assert Person.connection.index_exists?('people', 'new_idx', true)
|
||||
end
|
||||
end
|
||||
|
||||
def test_double_add_index
|
||||
unless current_adapter?(:OpenBaseAdapter)
|
||||
Person.connection.add_index('people', [:first_name], :name => 'some_idx')
|
||||
assert_nothing_raised { Person.connection.add_index('people', [:first_name], :name => 'some_idx') }
|
||||
end
|
||||
end
|
||||
|
||||
def testing_table_with_only_foo_attribute
|
||||
Person.connection.create_table :testings, :id => false do |t|
|
||||
t.column :foo, :string
|
||||
|
@ -311,6 +353,13 @@ if ActiveRecord::Base.connection.supports_migrations?
|
|||
Person.connection.drop_table table_name rescue nil
|
||||
end
|
||||
|
||||
def test_create_table_without_a_block
|
||||
table_name = :testings
|
||||
Person.connection.create_table table_name
|
||||
ensure
|
||||
Person.connection.drop_table table_name rescue nil
|
||||
end
|
||||
|
||||
# Sybase, and SQLite3 will not allow you to add a NOT NULL
|
||||
# column to a table without a default value.
|
||||
unless current_adapter?(:SybaseAdapter, :SQLiteAdapter)
|
||||
|
@ -516,6 +565,53 @@ if ActiveRecord::Base.connection.supports_migrations?
|
|||
assert !Person.column_methods_hash.include?(:last_name)
|
||||
end
|
||||
|
||||
if current_adapter?(:MysqlAdapter)
|
||||
def testing_table_for_positioning
|
||||
Person.connection.create_table :testings, :id => false do |t|
|
||||
t.column :first, :integer
|
||||
t.column :second, :integer
|
||||
t.column :third, :integer
|
||||
end
|
||||
|
||||
yield Person.connection
|
||||
ensure
|
||||
Person.connection.drop_table :testings rescue nil
|
||||
end
|
||||
protected :testing_table_for_positioning
|
||||
|
||||
def test_column_positioning
|
||||
testing_table_for_positioning do |conn|
|
||||
assert_equal %w(first second third), conn.columns(:testings).map {|c| c.name }
|
||||
end
|
||||
end
|
||||
|
||||
def test_add_column_with_positioning
|
||||
testing_table_for_positioning do |conn|
|
||||
conn.add_column :testings, :new_col, :integer
|
||||
assert_equal %w(first second third new_col), conn.columns(:testings).map {|c| c.name }
|
||||
end
|
||||
testing_table_for_positioning do |conn|
|
||||
conn.add_column :testings, :new_col, :integer, :first => true
|
||||
assert_equal %w(new_col first second third), conn.columns(:testings).map {|c| c.name }
|
||||
end
|
||||
testing_table_for_positioning do |conn|
|
||||
conn.add_column :testings, :new_col, :integer, :after => :first
|
||||
assert_equal %w(first new_col second third), conn.columns(:testings).map {|c| c.name }
|
||||
end
|
||||
end
|
||||
|
||||
def test_change_column_with_positioning
|
||||
testing_table_for_positioning do |conn|
|
||||
conn.change_column :testings, :second, :integer, :first => true
|
||||
assert_equal %w(second first third), conn.columns(:testings).map {|c| c.name }
|
||||
end
|
||||
testing_table_for_positioning do |conn|
|
||||
conn.change_column :testings, :second, :integer, :after => :third
|
||||
assert_equal %w(first third second), conn.columns(:testings).map {|c| c.name }
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
def test_add_rename
|
||||
Person.delete_all
|
||||
|
||||
|
@ -1023,6 +1119,25 @@ if ActiveRecord::Base.connection.supports_migrations?
|
|||
load(MIGRATIONS_ROOT + "/valid/1_people_have_last_names.rb")
|
||||
end
|
||||
|
||||
def test_target_version_zero_should_run_only_once
|
||||
# migrate up to 1
|
||||
ActiveRecord::Migrator.migrate(MIGRATIONS_ROOT + "/valid", 1)
|
||||
|
||||
# migrate down to 0
|
||||
ActiveRecord::Migrator.migrate(MIGRATIONS_ROOT + "/valid", 0)
|
||||
|
||||
# now unload the migrations that have been defined
|
||||
PeopleHaveLastNames.unloadable
|
||||
ActiveSupport::Dependencies.remove_unloadable_constants!
|
||||
|
||||
# migrate down to 0 again
|
||||
ActiveRecord::Migrator.migrate(MIGRATIONS_ROOT + "/valid", 0)
|
||||
|
||||
assert !defined? PeopleHaveLastNames
|
||||
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")
|
||||
|
||||
|
|
|
@ -78,4 +78,32 @@ class ModulesTest < ActiveRecord::TestCase
|
|||
end
|
||||
end
|
||||
end
|
||||
|
||||
def test_module_table_name_prefix
|
||||
assert_equal 'prefixed_companies', MyApplication::Business::Prefixed::Company.table_name, 'inferred table_name for ActiveRecord model in module with table_name_prefix'
|
||||
assert_equal 'prefixed_companies', MyApplication::Business::Prefixed::Nested::Company.table_name, 'table_name for ActiveRecord model in nested module with a parent table_name_prefix'
|
||||
assert_equal 'companies', MyApplication::Business::Prefixed::Firm.table_name, 'explicit table_name for ActiveRecord model in module with table_name_prefix should not be prefixed'
|
||||
end
|
||||
|
||||
def test_module_table_name_prefix_with_global_prefix
|
||||
classes = [ MyApplication::Business::Company,
|
||||
MyApplication::Business::Firm,
|
||||
MyApplication::Business::Client,
|
||||
MyApplication::Business::Client::Contact,
|
||||
MyApplication::Business::Developer,
|
||||
MyApplication::Business::Project,
|
||||
MyApplication::Business::Prefixed::Company,
|
||||
MyApplication::Business::Prefixed::Nested::Company,
|
||||
MyApplication::Billing::Account ]
|
||||
|
||||
ActiveRecord::Base.table_name_prefix = 'global_'
|
||||
classes.each(&:reset_table_name)
|
||||
assert_equal 'global_companies', MyApplication::Business::Company.table_name, 'inferred table_name for ActiveRecord model in module without table_name_prefix'
|
||||
assert_equal 'prefixed_companies', MyApplication::Business::Prefixed::Company.table_name, 'inferred table_name for ActiveRecord model in module with table_name_prefix'
|
||||
assert_equal 'prefixed_companies', MyApplication::Business::Prefixed::Nested::Company.table_name, 'table_name for ActiveRecord model in nested module with a parent table_name_prefix'
|
||||
assert_equal 'companies', MyApplication::Business::Prefixed::Firm.table_name, 'explicit table_name for ActiveRecord model in module with table_name_prefix should not be prefixed'
|
||||
ensure
|
||||
ActiveRecord::Base.table_name_prefix = ''
|
||||
classes.each(&:reset_table_name)
|
||||
end
|
||||
end
|
||||
|
|
|
@ -265,7 +265,7 @@ class NamedScopeTest < ActiveRecord::TestCase
|
|||
end
|
||||
|
||||
def test_rand_should_select_a_random_object_from_proxy
|
||||
assert Topic.approved.rand.is_a?(Topic)
|
||||
assert Topic.approved.random_element.is_a?(Topic)
|
||||
end
|
||||
|
||||
def test_should_use_where_in_query_for_named_scope
|
||||
|
|
|
@ -1,9 +1,14 @@
|
|||
require "cases/helper"
|
||||
require "models/pirate"
|
||||
require "models/ship"
|
||||
require "models/ship_part"
|
||||
require "models/bird"
|
||||
require "models/parrot"
|
||||
require "models/treasure"
|
||||
require "models/man"
|
||||
require "models/interest"
|
||||
require "models/owner"
|
||||
require "models/pet"
|
||||
|
||||
module AssertRaiseWithMessage
|
||||
def assert_raise_with_message(expected_exception, expected_message)
|
||||
|
@ -31,11 +36,30 @@ class TestNestedAttributesInGeneral < ActiveRecord::TestCase
|
|||
end
|
||||
|
||||
def test_should_add_a_proc_to_nested_attributes_options
|
||||
assert_equal ActiveRecord::NestedAttributes::ClassMethods::REJECT_ALL_BLANK_PROC,
|
||||
Pirate.nested_attributes_options[:birds_with_reject_all_blank][:reject_if]
|
||||
|
||||
[:parrots, :birds].each do |name|
|
||||
assert_instance_of Proc, Pirate.nested_attributes_options[name][:reject_if]
|
||||
end
|
||||
end
|
||||
|
||||
def test_should_not_build_a_new_record_if_reject_all_blank_returns_false
|
||||
pirate = Pirate.create!(:catchphrase => "Don' botharrr talkin' like one, savvy?")
|
||||
pirate.birds_with_reject_all_blank_attributes = [{:name => '', :color => ''}]
|
||||
pirate.save!
|
||||
|
||||
assert pirate.birds_with_reject_all_blank.empty?
|
||||
end
|
||||
|
||||
def test_should_build_a_new_record_if_reject_all_blank_does_not_return_false
|
||||
pirate = Pirate.create!(:catchphrase => "Don' botharrr talkin' like one, savvy?")
|
||||
pirate.birds_with_reject_all_blank_attributes = [{:name => 'Tweetie', :color => ''}]
|
||||
pirate.save!
|
||||
|
||||
assert_equal 1, pirate.birds_with_reject_all_blank.count
|
||||
end
|
||||
|
||||
def test_should_raise_an_ArgumentError_for_non_existing_associations
|
||||
assert_raise_with_message ArgumentError, "No association found for name `honesty'. Has it been defined yet?" do
|
||||
Pirate.accepts_nested_attributes_for :honesty
|
||||
|
@ -60,12 +84,6 @@ class TestNestedAttributesInGeneral < ActiveRecord::TestCase
|
|||
assert ship._destroy
|
||||
end
|
||||
|
||||
def test_underscore_delete_is_deprecated
|
||||
ActiveSupport::Deprecation.expects(:warn)
|
||||
ship = Ship.create!(:name => 'Nights Dirty Lightning')
|
||||
ship._delete
|
||||
end
|
||||
|
||||
def test_reject_if_method_without_arguments
|
||||
Pirate.accepts_nested_attributes_for :ship, :reject_if => :new_record?
|
||||
|
||||
|
@ -157,6 +175,12 @@ class TestNestedAttributesOnAHasOneAssociation < ActiveRecord::TestCase
|
|||
assert_equal 'Davy Jones Gold Dagger', @pirate.ship.name
|
||||
end
|
||||
|
||||
def test_should_raise_RecordNotFound_if_an_id_is_given_but_doesnt_return_a_record
|
||||
assert_raise_with_message ActiveRecord::RecordNotFound, "Couldn't find Ship with ID=1234567890 for Pirate with ID=#{@pirate.id}" do
|
||||
@pirate.ship_attributes = { :id => 1234567890 }
|
||||
end
|
||||
end
|
||||
|
||||
def test_should_take_a_hash_with_string_keys_and_update_the_associated_model
|
||||
@pirate.reload.ship_attributes = { 'id' => @ship.id, 'name' => 'Davy Jones Gold Dagger' }
|
||||
|
||||
|
@ -226,9 +250,32 @@ class TestNestedAttributesOnAHasOneAssociation < ActiveRecord::TestCase
|
|||
def test_should_automatically_enable_autosave_on_the_association
|
||||
assert Pirate.reflect_on_association(:ship).options[:autosave]
|
||||
end
|
||||
|
||||
def test_should_accept_update_only_option
|
||||
@pirate.update_attribute(:update_only_ship_attributes, { :id => @pirate.ship.id, :name => 'Mayflower' })
|
||||
end
|
||||
|
||||
def test_should_create_new_model_when_nothing_is_there_and_update_only_is_true
|
||||
@ship.delete
|
||||
assert_difference('Ship.count', 1) do
|
||||
@pirate.reload.update_attribute(:update_only_ship_attributes, { :name => 'Mayflower' })
|
||||
end
|
||||
end
|
||||
|
||||
def test_should_update_existing_when_update_only_is_true_and_no_id_is_given
|
||||
@ship.delete
|
||||
@ship = @pirate.create_update_only_ship(:name => 'Nights Dirty Lightning')
|
||||
|
||||
assert_no_difference('Ship.count') do
|
||||
@pirate.update_attributes(:update_only_ship_attributes => { :name => 'Mayflower' })
|
||||
end
|
||||
assert_equal 'Mayflower', @ship.reload.name
|
||||
end
|
||||
end
|
||||
|
||||
class TestNestedAttributesOnABelongsToAssociation < ActiveRecord::TestCase
|
||||
include AssertRaiseWithMessage
|
||||
|
||||
def setup
|
||||
@ship = Ship.new(:name => 'Nights Dirty Lightning')
|
||||
@pirate = @ship.build_pirate(:catchphrase => 'Aye')
|
||||
|
@ -283,6 +330,12 @@ class TestNestedAttributesOnABelongsToAssociation < ActiveRecord::TestCase
|
|||
assert_equal 'Arr', @ship.pirate.catchphrase
|
||||
end
|
||||
|
||||
def test_should_raise_RecordNotFound_if_an_id_is_given_but_doesnt_return_a_record
|
||||
assert_raise_with_message ActiveRecord::RecordNotFound, "Couldn't find Pirate with ID=1234567890 for Ship with ID=#{@ship.id}" do
|
||||
@ship.pirate_attributes = { :id => 1234567890 }
|
||||
end
|
||||
end
|
||||
|
||||
def test_should_take_a_hash_with_string_keys_and_update_the_associated_model
|
||||
@ship.reload.pirate_attributes = { 'id' => @pirate.id, 'catchphrase' => 'Arr' }
|
||||
|
||||
|
@ -343,6 +396,23 @@ class TestNestedAttributesOnABelongsToAssociation < ActiveRecord::TestCase
|
|||
def test_should_automatically_enable_autosave_on_the_association
|
||||
assert Ship.reflect_on_association(:pirate).options[:autosave]
|
||||
end
|
||||
|
||||
def test_should_create_new_model_when_nothing_is_there_and_update_only_is_true
|
||||
@pirate.delete
|
||||
assert_difference('Pirate.count', 1) do
|
||||
@ship.reload.update_attribute(:update_only_pirate_attributes, { :catchphrase => 'Arr' })
|
||||
end
|
||||
end
|
||||
|
||||
def test_should_update_existing_when_update_only_is_true_and_no_id_is_given
|
||||
@pirate.delete
|
||||
@pirate = @ship.create_update_only_pirate(:catchphrase => 'Aye')
|
||||
|
||||
assert_no_difference('Pirate.count') do
|
||||
@ship.update_attributes(:update_only_pirate_attributes => { :catchphrase => 'Arr' })
|
||||
end
|
||||
assert_equal 'Arr', @pirate.reload.catchphrase
|
||||
end
|
||||
end
|
||||
|
||||
module NestedAttributesOnACollectionAssociationTests
|
||||
|
@ -352,6 +422,15 @@ module NestedAttributesOnACollectionAssociationTests
|
|||
assert_respond_to @pirate, association_setter
|
||||
end
|
||||
|
||||
def test_should_save_only_one_association_on_create
|
||||
pirate = Pirate.create!({
|
||||
:catchphrase => 'Arr',
|
||||
association_getter => { 'foo' => { :name => 'Grace OMalley' } }
|
||||
})
|
||||
|
||||
assert_equal 1, pirate.reload.send(@association_name).count
|
||||
end
|
||||
|
||||
def test_should_take_a_hash_with_string_keys_and_assign_the_attributes_to_the_associated_models
|
||||
@alternate_params[association_getter].stringify_keys!
|
||||
@pirate.update_attributes @alternate_params
|
||||
|
@ -376,6 +455,16 @@ module NestedAttributesOnACollectionAssociationTests
|
|||
assert_equal 'Privateers Greed', @pirate.send(@association_name).last.name
|
||||
end
|
||||
|
||||
def test_should_not_load_association_when_updating_existing_records
|
||||
@pirate.reload
|
||||
@pirate.send(association_setter, [{ :id => @child_1.id, :name => 'Grace OMalley' }])
|
||||
assert ! @pirate.send(@association_name).loaded?
|
||||
|
||||
@pirate.save
|
||||
assert ! @pirate.send(@association_name).loaded?
|
||||
assert_equal 'Grace OMalley', @child_1.reload.name
|
||||
end
|
||||
|
||||
def test_should_take_a_hash_with_composite_id_keys_and_assign_the_attributes_to_the_associated_models
|
||||
@child_1.stubs(:id).returns('ABC1X')
|
||||
@child_2.stubs(:id).returns('ABC2X')
|
||||
|
@ -390,6 +479,12 @@ module NestedAttributesOnACollectionAssociationTests
|
|||
assert_equal ['Grace OMalley', 'Privateers Greed'], [@child_1.name, @child_2.name]
|
||||
end
|
||||
|
||||
def test_should_raise_RecordNotFound_if_an_id_is_given_but_doesnt_return_a_record
|
||||
assert_raise_with_message ActiveRecord::RecordNotFound, "Couldn't find #{@child_1.class.name} with ID=1234567890 for Pirate with ID=#{@pirate.id}" do
|
||||
@pirate.attributes = { association_getter => [{ :id => 1234567890 }] }
|
||||
end
|
||||
end
|
||||
|
||||
def test_should_automatically_build_new_associated_models_for_each_entry_in_a_hash_where_the_id_is_missing
|
||||
@pirate.send(@association_name).destroy_all
|
||||
@pirate.reload.attributes = {
|
||||
|
@ -424,7 +519,7 @@ module NestedAttributesOnACollectionAssociationTests
|
|||
|
||||
def test_should_ignore_new_associated_records_if_a_reject_if_proc_returns_false
|
||||
@alternate_params[association_getter]['baz'] = {}
|
||||
assert_no_difference("@pirate.send(@association_name).length") do
|
||||
assert_no_difference("@pirate.send(@association_name).count") do
|
||||
@pirate.attributes = @alternate_params
|
||||
end
|
||||
end
|
||||
|
@ -495,6 +590,41 @@ module NestedAttributesOnACollectionAssociationTests
|
|||
assert Pirate.reflect_on_association(@association_name).options[:autosave]
|
||||
end
|
||||
|
||||
def test_validate_presence_of_parent_works_with_inverse_of
|
||||
Man.accepts_nested_attributes_for(:interests)
|
||||
assert_equal :man, Man.reflect_on_association(:interests).options[:inverse_of]
|
||||
assert_equal :interests, Interest.reflect_on_association(:man).options[:inverse_of]
|
||||
|
||||
repair_validations(Interest) do
|
||||
Interest.validates_presence_of(:man)
|
||||
assert_difference 'Man.count' do
|
||||
assert_difference 'Interest.count', 2 do
|
||||
man = Man.create!(:name => 'John',
|
||||
:interests_attributes => [{:topic=>'Cars'}, {:topic=>'Sports'}])
|
||||
assert_equal 2, man.interests.count
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
def test_validate_presence_of_parent_fails_without_inverse_of
|
||||
Man.accepts_nested_attributes_for(:interests)
|
||||
Man.reflect_on_association(:interests).options.delete(:inverse_of)
|
||||
Interest.reflect_on_association(:man).options.delete(:inverse_of)
|
||||
|
||||
repair_validations(Interest) do
|
||||
Interest.validates_presence_of(:man)
|
||||
assert_no_difference ['Man.count', 'Interest.count'] do
|
||||
man = Man.create(:name => 'John',
|
||||
:interests_attributes => [{:topic=>'Cars'}, {:topic=>'Sports'}])
|
||||
assert !man.errors[:'interests.man'].empty?
|
||||
end
|
||||
end
|
||||
# restore :inverse_of
|
||||
Man.reflect_on_association(:interests).options[:inverse_of] = :man
|
||||
Interest.reflect_on_association(:man).options[:inverse_of] = :interests
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
def association_setter
|
||||
|
@ -579,3 +709,105 @@ class TestNestedAttributesLimit < ActiveRecord::TestCase
|
|||
end
|
||||
end
|
||||
end
|
||||
|
||||
class TestNestedAttributesWithNonStandardPrimaryKeys < ActiveRecord::TestCase
|
||||
fixtures :owners, :pets
|
||||
|
||||
def setup
|
||||
Owner.accepts_nested_attributes_for :pets
|
||||
|
||||
@owner = owners(:ashley)
|
||||
@pet1, @pet2 = pets(:chew), pets(:mochi)
|
||||
|
||||
@params = {
|
||||
:pets_attributes => {
|
||||
'0' => { :id => @pet1.id, :name => 'Foo' },
|
||||
'1' => { :id => @pet2.id, :name => 'Bar' }
|
||||
}
|
||||
}
|
||||
end
|
||||
|
||||
def test_should_update_existing_records_with_non_standard_primary_key
|
||||
@owner.update_attributes(@params)
|
||||
assert_equal ['Foo', 'Bar'], @owner.pets.map(&:name)
|
||||
end
|
||||
end
|
||||
|
||||
class TestHasOneAutosaveAssoictaionWhichItselfHasAutosaveAssociations < ActiveRecord::TestCase
|
||||
self.use_transactional_fixtures = false
|
||||
|
||||
def setup
|
||||
@pirate = Pirate.create!(:catchphrase => "My baby takes tha mornin' train!")
|
||||
@ship = @pirate.create_ship(:name => "The good ship Dollypop")
|
||||
@part = @ship.parts.create!(:name => "Mast")
|
||||
@trinket = @part.trinkets.create!(:name => "Necklace")
|
||||
end
|
||||
|
||||
test "when great-grandchild changed in memory, saving parent should save great-grandchild" do
|
||||
@trinket.name = "changed"
|
||||
@pirate.save
|
||||
assert_equal "changed", @trinket.reload.name
|
||||
end
|
||||
|
||||
test "when great-grandchild changed via attributes, saving parent should save great-grandchild" do
|
||||
@pirate.attributes = {:ship_attributes => {:id => @ship.id, :parts_attributes => [{:id => @part.id, :trinkets_attributes => [{:id => @trinket.id, :name => "changed"}]}]}}
|
||||
@pirate.save
|
||||
assert_equal "changed", @trinket.reload.name
|
||||
end
|
||||
|
||||
test "when great-grandchild marked_for_destruction via attributes, saving parent should destroy great-grandchild" do
|
||||
@pirate.attributes = {:ship_attributes => {:id => @ship.id, :parts_attributes => [{:id => @part.id, :trinkets_attributes => [{:id => @trinket.id, :_destroy => true}]}]}}
|
||||
assert_difference('@part.trinkets.count', -1) { @pirate.save }
|
||||
end
|
||||
|
||||
test "when great-grandchild added via attributes, saving parent should create great-grandchild" do
|
||||
@pirate.attributes = {:ship_attributes => {:id => @ship.id, :parts_attributes => [{:id => @part.id, :trinkets_attributes => [{:name => "created"}]}]}}
|
||||
assert_difference('@part.trinkets.count', 1) { @pirate.save }
|
||||
end
|
||||
|
||||
test "when extra records exist for associations, validate (which calls nested_records_changed_for_autosave?) should not load them up" do
|
||||
@trinket.name = "changed"
|
||||
Ship.create!(:pirate => @pirate, :name => "The Black Rock")
|
||||
ShipPart.create!(:ship => @ship, :name => "Stern")
|
||||
assert_no_queries { @pirate.valid? }
|
||||
end
|
||||
end
|
||||
|
||||
class TestHasManyAutosaveAssoictaionWhichItselfHasAutosaveAssociations < ActiveRecord::TestCase
|
||||
self.use_transactional_fixtures = false
|
||||
|
||||
def setup
|
||||
@ship = Ship.create!(:name => "The good ship Dollypop")
|
||||
@part = @ship.parts.create!(:name => "Mast")
|
||||
@trinket = @part.trinkets.create!(:name => "Necklace")
|
||||
end
|
||||
|
||||
test "when grandchild changed in memory, saving parent should save grandchild" do
|
||||
@trinket.name = "changed"
|
||||
@ship.save
|
||||
assert_equal "changed", @trinket.reload.name
|
||||
end
|
||||
|
||||
test "when grandchild changed via attributes, saving parent should save grandchild" do
|
||||
@ship.attributes = {:parts_attributes => [{:id => @part.id, :trinkets_attributes => [{:id => @trinket.id, :name => "changed"}]}]}
|
||||
@ship.save
|
||||
assert_equal "changed", @trinket.reload.name
|
||||
end
|
||||
|
||||
test "when grandchild marked_for_destruction via attributes, saving parent should destroy grandchild" do
|
||||
@ship.attributes = {:parts_attributes => [{:id => @part.id, :trinkets_attributes => [{:id => @trinket.id, :_destroy => true}]}]}
|
||||
assert_difference('@part.trinkets.count', -1) { @ship.save }
|
||||
end
|
||||
|
||||
test "when grandchild added via attributes, saving parent should create grandchild" do
|
||||
@ship.attributes = {:parts_attributes => [{:id => @part.id, :trinkets_attributes => [{:name => "created"}]}]}
|
||||
assert_difference('@part.trinkets.count', 1) { @ship.save }
|
||||
end
|
||||
|
||||
test "when extra records exist for associations, validate (which calls nested_records_changed_for_autosave?) should not load them up" do
|
||||
@trinket.name = "changed"
|
||||
Ship.create!(:name => "The Black Rock")
|
||||
ShipPart.create!(:ship => @ship, :name => "Stern")
|
||||
assert_no_queries { @ship.valid? }
|
||||
end
|
||||
end
|
|
@ -49,8 +49,14 @@ class QueryCacheTest < ActiveRecord::TestCase
|
|||
end
|
||||
|
||||
def test_cache_does_not_wrap_string_results_in_arrays
|
||||
require 'sqlite3/version' if current_adapter?(:SQLite3Adapter)
|
||||
|
||||
Task.cache do
|
||||
assert_instance_of String, Task.connection.select_value("SELECT count(*) AS count_all FROM tasks")
|
||||
if current_adapter?(:SQLite3Adapter) && SQLite3::Version::VERSION > '1.2.5'
|
||||
assert_instance_of Fixnum, Task.connection.select_value("SELECT count(*) AS count_all FROM tasks")
|
||||
else
|
||||
assert_instance_of String, Task.connection.select_value("SELECT count(*) AS count_all FROM tasks")
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
|
|
@ -4,9 +4,12 @@ require 'models/customer'
|
|||
require 'models/company'
|
||||
require 'models/company_in_module'
|
||||
require 'models/subscriber'
|
||||
require 'models/ship'
|
||||
require 'models/pirate'
|
||||
|
||||
class ReflectionTest < ActiveRecord::TestCase
|
||||
include ActiveRecord::Reflection
|
||||
|
||||
fixtures :topics, :customers, :companies, :subscribers
|
||||
|
||||
def setup
|
||||
|
@ -62,22 +65,22 @@ class ReflectionTest < ActiveRecord::TestCase
|
|||
end
|
||||
|
||||
def test_reflection_klass_for_nested_class_name
|
||||
reflection = ActiveRecord::Reflection::MacroReflection.new(nil, nil, { :class_name => 'MyApplication::Business::Company' }, nil)
|
||||
reflection = MacroReflection.new(nil, nil, { :class_name => 'MyApplication::Business::Company' }, nil)
|
||||
assert_nothing_raised do
|
||||
assert_equal MyApplication::Business::Company, reflection.klass
|
||||
end
|
||||
end
|
||||
|
||||
def test_aggregation_reflection
|
||||
reflection_for_address = ActiveRecord::Reflection::AggregateReflection.new(
|
||||
reflection_for_address = AggregateReflection.new(
|
||||
:composed_of, :address, { :mapping => [ %w(address_street street), %w(address_city city), %w(address_country country) ] }, Customer
|
||||
)
|
||||
|
||||
reflection_for_balance = ActiveRecord::Reflection::AggregateReflection.new(
|
||||
reflection_for_balance = AggregateReflection.new(
|
||||
:composed_of, :balance, { :class_name => "Money", :mapping => %w(balance amount) }, Customer
|
||||
)
|
||||
|
||||
reflection_for_gps_location = ActiveRecord::Reflection::AggregateReflection.new(
|
||||
reflection_for_gps_location = AggregateReflection.new(
|
||||
:composed_of, :gps_location, { }, Customer
|
||||
)
|
||||
|
||||
|
@ -102,7 +105,7 @@ class ReflectionTest < ActiveRecord::TestCase
|
|||
end
|
||||
|
||||
def test_has_many_reflection
|
||||
reflection_for_clients = ActiveRecord::Reflection::AssociationReflection.new(:has_many, :clients, { :order => "id", :dependent => :destroy }, Firm)
|
||||
reflection_for_clients = AssociationReflection.new(:has_many, :clients, { :order => "id", :dependent => :destroy }, Firm)
|
||||
|
||||
assert_equal reflection_for_clients, Firm.reflect_on_association(:clients)
|
||||
|
||||
|
@ -114,7 +117,7 @@ class ReflectionTest < ActiveRecord::TestCase
|
|||
end
|
||||
|
||||
def test_has_one_reflection
|
||||
reflection_for_account = ActiveRecord::Reflection::AssociationReflection.new(:has_one, :account, { :foreign_key => "firm_id", :dependent => :destroy }, Firm)
|
||||
reflection_for_account = AssociationReflection.new(:has_one, :account, { :foreign_key => "firm_id", :dependent => :destroy }, Firm)
|
||||
assert_equal reflection_for_account, Firm.reflect_on_association(:account)
|
||||
|
||||
assert_equal Account, Firm.reflect_on_association(:account).klass
|
||||
|
@ -181,7 +184,44 @@ class ReflectionTest < ActiveRecord::TestCase
|
|||
end
|
||||
|
||||
def test_has_many_through_reflection
|
||||
assert_kind_of ActiveRecord::Reflection::ThroughReflection, Subscriber.reflect_on_association(:books)
|
||||
assert_kind_of ThroughReflection, Subscriber.reflect_on_association(:books)
|
||||
end
|
||||
|
||||
def test_collection_association
|
||||
assert Pirate.reflect_on_association(:birds).collection?
|
||||
assert Pirate.reflect_on_association(:parrots).collection?
|
||||
|
||||
assert !Pirate.reflect_on_association(:ship).collection?
|
||||
assert !Ship.reflect_on_association(:pirate).collection?
|
||||
end
|
||||
|
||||
def test_default_association_validation
|
||||
assert AssociationReflection.new(:has_many, :clients, {}, Firm).validate?
|
||||
|
||||
assert !AssociationReflection.new(:has_one, :client, {}, Firm).validate?
|
||||
assert !AssociationReflection.new(:belongs_to, :client, {}, Firm).validate?
|
||||
assert !AssociationReflection.new(:has_and_belongs_to_many, :clients, {}, Firm).validate?
|
||||
end
|
||||
|
||||
def test_always_validate_association_if_explicit
|
||||
assert AssociationReflection.new(:has_one, :client, { :validate => true }, Firm).validate?
|
||||
assert AssociationReflection.new(:belongs_to, :client, { :validate => true }, Firm).validate?
|
||||
assert AssociationReflection.new(:has_many, :clients, { :validate => true }, Firm).validate?
|
||||
assert AssociationReflection.new(:has_and_belongs_to_many, :clients, { :validate => true }, Firm).validate?
|
||||
end
|
||||
|
||||
def test_validate_association_if_autosave
|
||||
assert AssociationReflection.new(:has_one, :client, { :autosave => true }, Firm).validate?
|
||||
assert AssociationReflection.new(:belongs_to, :client, { :autosave => true }, Firm).validate?
|
||||
assert AssociationReflection.new(:has_many, :clients, { :autosave => true }, Firm).validate?
|
||||
assert AssociationReflection.new(:has_and_belongs_to_many, :clients, { :autosave => true }, Firm).validate?
|
||||
end
|
||||
|
||||
def test_never_validate_association_if_explicit
|
||||
assert !AssociationReflection.new(:has_one, :client, { :autosave => true, :validate => false }, Firm).validate?
|
||||
assert !AssociationReflection.new(:belongs_to, :client, { :autosave => true, :validate => false }, Firm).validate?
|
||||
assert !AssociationReflection.new(:has_many, :clients, { :autosave => true, :validate => false }, Firm).validate?
|
||||
assert !AssociationReflection.new(:has_and_belongs_to_many, :clients, { :autosave => true, :validate => false }, Firm).validate?
|
||||
end
|
||||
|
||||
private
|
||||
|
|
|
@ -137,11 +137,11 @@ class SchemaTest < ActiveRecord::TestCase
|
|||
|
||||
def test_with_uppercase_index_name
|
||||
ActiveRecord::Base.connection.execute "CREATE INDEX \"things_Index\" ON #{SCHEMA_NAME}.things (name)"
|
||||
assert_nothing_raised { ActiveRecord::Base.connection.remove_index :things, :name => "#{SCHEMA_NAME}.things_Index"}
|
||||
assert_nothing_raised { ActiveRecord::Base.connection.remove_index! "things", "#{SCHEMA_NAME}.things_Index"}
|
||||
|
||||
ActiveRecord::Base.connection.execute "CREATE INDEX \"things_Index\" ON #{SCHEMA_NAME}.things (name)"
|
||||
ActiveRecord::Base.connection.schema_search_path = SCHEMA_NAME
|
||||
assert_nothing_raised { ActiveRecord::Base.connection.remove_index :things, :name => "things_Index"}
|
||||
assert_nothing_raised { ActiveRecord::Base.connection.remove_index! "things", "things_Index"}
|
||||
ActiveRecord::Base.connection.schema_search_path = "public"
|
||||
end
|
||||
|
||||
|
|
|
@ -27,42 +27,6 @@ module ActiveRecordValidationsI18nTestHelper
|
|||
end
|
||||
end
|
||||
|
||||
# DEPRECATIONS
|
||||
|
||||
class ActiveRecordValidationsI18nDeprecationsTests < ActiveSupport::TestCase
|
||||
test "default_error_messages is deprecated and can be removed in Rails 3 / ActiveModel" do
|
||||
assert_deprecated('ActiveRecord::Errors.default_error_messages') do
|
||||
ActiveRecord::Errors.default_error_messages
|
||||
end
|
||||
end
|
||||
|
||||
test "%s interpolation syntax in error messages still works" do
|
||||
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
|
||||
|
||||
test "%s interpolation syntax in error messages is deprecated" do
|
||||
assert_deprecated('using %s in messages') do
|
||||
I18n.t :does_not_exist, :default => "%s interpolation syntax is deprected", :value => 'this'
|
||||
end
|
||||
end
|
||||
|
||||
test "%d interpolation syntax in error messages still works" do
|
||||
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
|
||||
|
||||
test "%d interpolation syntax in error messages is deprecated" do
|
||||
assert_deprecated('using %d in messages') do
|
||||
I18n.t :does_not_exist, :default => "%d interpolation syntaxes are deprected", :count => 2
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
|
||||
# ACTIVERECORD VALIDATIONS
|
||||
#
|
||||
|
@ -521,6 +485,12 @@ class ActiveRecordErrorI18nTests < ActiveSupport::TestCase
|
|||
assert_equal message, ActiveRecord::Error.new(@reply, *args).full_message
|
||||
end
|
||||
|
||||
test ":default is only given to message if a symbol is supplied" do
|
||||
store_translations(:errors => { :messages => { :"foo bar" => "You fooed: {{value}}." } })
|
||||
@reply.errors.add(:title, :inexistent, :default => "foo bar")
|
||||
assert_equal "foo bar", @reply.errors[:title]
|
||||
end
|
||||
|
||||
test "#generate_message passes the model attribute value for interpolation" do
|
||||
store_translations(:errors => { :messages => { :foo => "You fooed: {{value}}." } })
|
||||
@reply.title = "da title"
|
||||
|
|
|
@ -9,6 +9,8 @@ require 'models/guid'
|
|||
require 'models/owner'
|
||||
require 'models/pet'
|
||||
require 'models/event'
|
||||
require 'models/man'
|
||||
require 'models/interest'
|
||||
|
||||
# The following methods in Topic are used in test_conditional_validation_*
|
||||
class Topic
|
||||
|
@ -186,7 +188,7 @@ class ValidationsTest < ActiveRecord::TestCase
|
|||
end
|
||||
end
|
||||
|
||||
def test_single_error_per_attr_iteration
|
||||
def test_single_error_string_per_attr_iteration
|
||||
r = Reply.new
|
||||
r.save
|
||||
|
||||
|
@ -197,6 +199,17 @@ class ValidationsTest < ActiveRecord::TestCase
|
|||
assert errors.include?(["content", "Empty"])
|
||||
end
|
||||
|
||||
def test_single_error_object_per_attr_iteration
|
||||
r = Reply.new
|
||||
r.save
|
||||
|
||||
errors = []
|
||||
r.errors.each_error { |attr, error| errors << [attr, error.attribute] }
|
||||
|
||||
assert errors.include?(["title", "title"])
|
||||
assert errors.include?(["content", "content"])
|
||||
end
|
||||
|
||||
def test_multiple_errors_per_attr_iteration_with_full_error_composition
|
||||
r = Reply.new
|
||||
r.title = "Wrong Create"
|
||||
|
@ -356,6 +369,25 @@ class ValidationsTest < ActiveRecord::TestCase
|
|||
assert t.save
|
||||
end
|
||||
|
||||
def test_validates_presence_of_belongs_to_association__parent_is_new_record
|
||||
repair_validations(Interest) do
|
||||
# Note that Interest and Man have the :inverse_of option set
|
||||
Interest.validates_presence_of(:man)
|
||||
man = Man.new(:name => 'John')
|
||||
interest = man.interests.build(:topic => 'Airplanes')
|
||||
assert interest.valid?, "Expected interest to be valid, but was not. Interest should have a man object associated"
|
||||
end
|
||||
end
|
||||
|
||||
def test_validates_presence_of_belongs_to_association__existing_parent
|
||||
repair_validations(Interest) do
|
||||
Interest.validates_presence_of(:man)
|
||||
man = Man.create!(:name => 'John')
|
||||
interest = man.interests.build(:topic => 'Airplanes')
|
||||
assert interest.valid?, "Expected interest to be valid, but was not. Interest should have a man object associated"
|
||||
end
|
||||
end
|
||||
|
||||
def test_validate_uniqueness
|
||||
Topic.validates_uniqueness_of(:title)
|
||||
|
||||
|
|
11
vendor/rails/activerecord/test/cases/yaml_serialization_test.rb
vendored
Normal file
11
vendor/rails/activerecord/test/cases/yaml_serialization_test.rb
vendored
Normal file
|
@ -0,0 +1,11 @@
|
|||
require "cases/helper"
|
||||
require 'models/topic'
|
||||
|
||||
class YamlSerializationTest < ActiveRecord::TestCase
|
||||
def test_to_yaml_with_time_with_zone_should_not_raise_exception
|
||||
Time.zone = ActiveSupport::TimeZone["Pacific Time (US & Canada)"]
|
||||
ActiveRecord::Base.time_zone_aware_attributes = true
|
||||
topic = Topic.new(:written_on => DateTime.now)
|
||||
assert_nothing_raised { topic.to_yaml }
|
||||
end
|
||||
end
|
11
vendor/rails/activerecord/test/fixtures/faces.yml
vendored
Normal file
11
vendor/rails/activerecord/test/fixtures/faces.yml
vendored
Normal file
|
@ -0,0 +1,11 @@
|
|||
trusting:
|
||||
description: trusting
|
||||
man: gordon
|
||||
|
||||
weather_beaten:
|
||||
description: weather beaten
|
||||
man: steve
|
||||
|
||||
confused:
|
||||
description: confused
|
||||
polymorphic_man: gordon (Man)
|
33
vendor/rails/activerecord/test/fixtures/interests.yml
vendored
Normal file
33
vendor/rails/activerecord/test/fixtures/interests.yml
vendored
Normal file
|
@ -0,0 +1,33 @@
|
|||
trainspotting:
|
||||
topic: Trainspotting
|
||||
zine: staying_in
|
||||
man: gordon
|
||||
|
||||
birdwatching:
|
||||
topic: Birdwatching
|
||||
zine: staying_in
|
||||
man: gordon
|
||||
|
||||
stamp_collecting:
|
||||
topic: Stamp Collecting
|
||||
zine: staying_in
|
||||
man: gordon
|
||||
|
||||
hunting:
|
||||
topic: Hunting
|
||||
zine: going_out
|
||||
man: steve
|
||||
|
||||
woodsmanship:
|
||||
topic: Woodsmanship
|
||||
zine: going_out
|
||||
man: steve
|
||||
|
||||
survival:
|
||||
topic: Survival
|
||||
zine: going_out
|
||||
man: steve
|
||||
|
||||
llama_wrangling:
|
||||
topic: Llama Wrangling
|
||||
polymorphic_man: gordon (Man)
|
5
vendor/rails/activerecord/test/fixtures/men.yml
vendored
Normal file
5
vendor/rails/activerecord/test/fixtures/men.yml
vendored
Normal file
|
@ -0,0 +1,5 @@
|
|||
gordon:
|
||||
name: Gordon
|
||||
|
||||
steve:
|
||||
name: Steve
|
5
vendor/rails/activerecord/test/fixtures/zines.yml
vendored
Normal file
5
vendor/rails/activerecord/test/fixtures/zines.yml
vendored
Normal file
|
@ -0,0 +1,5 @@
|
|||
staying_in:
|
||||
title: Staying in '08
|
||||
|
||||
going_out:
|
||||
title: Outdoor Pursuits 2k+8
|
|
@ -88,6 +88,9 @@ class Author < ActiveRecord::Base
|
|||
has_many :tags, :through => :posts # through has_many :through
|
||||
has_many :post_categories, :through => :posts, :source => :categories
|
||||
|
||||
has_many :event_authors
|
||||
has_many :events, :through => :event_authors
|
||||
|
||||
has_one :essay, :primary_key => :name, :as => :writer
|
||||
|
||||
belongs_to :author_address, :dependent => :destroy
|
||||
|
|
|
@ -1,3 +1,9 @@
|
|||
class Bird < ActiveRecord::Base
|
||||
validates_presence_of :name
|
||||
|
||||
attr_accessor :cancel_save_from_callback
|
||||
before_save :cancel_save_callback_method, :if => :cancel_save_from_callback
|
||||
def cancel_save_callback_method
|
||||
false
|
||||
end
|
||||
end
|
|
@ -30,6 +30,23 @@ module MyApplication
|
|||
has_and_belongs_to_many :developers
|
||||
end
|
||||
|
||||
module Prefixed
|
||||
def self.table_name_prefix
|
||||
'prefixed_'
|
||||
end
|
||||
|
||||
class Company < ActiveRecord::Base
|
||||
end
|
||||
|
||||
class Firm < Company
|
||||
self.table_name = 'companies'
|
||||
end
|
||||
|
||||
module Nested
|
||||
class Company < ActiveRecord::Base
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
module Billing
|
||||
|
|
5
vendor/rails/activerecord/test/models/event_author.rb
vendored
Normal file
5
vendor/rails/activerecord/test/models/event_author.rb
vendored
Normal file
|
@ -0,0 +1,5 @@
|
|||
class EventAuthor < ActiveRecord::Base
|
||||
belongs_to :author
|
||||
belongs_to :event
|
||||
end
|
||||
|
7
vendor/rails/activerecord/test/models/face.rb
vendored
Normal file
7
vendor/rails/activerecord/test/models/face.rb
vendored
Normal file
|
@ -0,0 +1,7 @@
|
|||
class Face < ActiveRecord::Base
|
||||
belongs_to :man, :inverse_of => :face
|
||||
belongs_to :polymorphic_man, :polymorphic => true, :inverse_of => :polymorphic_face
|
||||
# These is a "broken" inverse_of for the purposes of testing
|
||||
belongs_to :horrible_man, :class_name => 'Man', :inverse_of => :horrible_face
|
||||
belongs_to :horrible_polymorphic_man, :polymorphic => true, :inverse_of => :horrible_polymorphic_face
|
||||
end
|
5
vendor/rails/activerecord/test/models/interest.rb
vendored
Normal file
5
vendor/rails/activerecord/test/models/interest.rb
vendored
Normal file
|
@ -0,0 +1,5 @@
|
|||
class Interest < ActiveRecord::Base
|
||||
belongs_to :man, :inverse_of => :interests
|
||||
belongs_to :polymorphic_man, :polymorphic => true, :inverse_of => :polymorphic_interests
|
||||
belongs_to :zine, :inverse_of => :interests
|
||||
end
|
4
vendor/rails/activerecord/test/models/invoice.rb
vendored
Normal file
4
vendor/rails/activerecord/test/models/invoice.rb
vendored
Normal file
|
@ -0,0 +1,4 @@
|
|||
class Invoice < ActiveRecord::Base
|
||||
has_many :line_items, :autosave => true
|
||||
before_save {|record| record.balance = record.line_items.map(&:amount).sum }
|
||||
end
|
3
vendor/rails/activerecord/test/models/line_item.rb
vendored
Normal file
3
vendor/rails/activerecord/test/models/line_item.rb
vendored
Normal file
|
@ -0,0 +1,3 @@
|
|||
class LineItem < ActiveRecord::Base
|
||||
belongs_to :invoice, :touch => true
|
||||
end
|
9
vendor/rails/activerecord/test/models/man.rb
vendored
Normal file
9
vendor/rails/activerecord/test/models/man.rb
vendored
Normal file
|
@ -0,0 +1,9 @@
|
|||
class Man < ActiveRecord::Base
|
||||
has_one :face, :inverse_of => :man
|
||||
has_one :polymorphic_face, :class_name => 'Face', :as => :polymorphic_man, :inverse_of => :polymorphic_man
|
||||
has_many :interests, :inverse_of => :man
|
||||
has_many :polymorphic_interests, :class_name => 'Interest', :as => :polymorphic_man, :inverse_of => :polymorphic_man
|
||||
# These are "broken" inverse_of associations for the purposes of testing
|
||||
has_one :dirty_face, :class_name => 'Face', :inverse_of => :dirty_man
|
||||
has_many :secret_interests, :class_name => 'Interest', :inverse_of => :secret_man
|
||||
end
|
|
@ -6,6 +6,12 @@ class Parrot < ActiveRecord::Base
|
|||
alias_attribute :title, :name
|
||||
|
||||
validates_presence_of :name
|
||||
|
||||
attr_accessor :cancel_save_from_callback
|
||||
before_save :cancel_save_callback_method, :if => :cancel_save_from_callback
|
||||
def cancel_save_callback_method
|
||||
false
|
||||
end
|
||||
end
|
||||
|
||||
class LiveParrot < Parrot
|
||||
|
|
10
vendor/rails/activerecord/test/models/pirate.rb
vendored
10
vendor/rails/activerecord/test/models/pirate.rb
vendored
|
@ -19,6 +19,7 @@ class Pirate < ActiveRecord::Base
|
|||
|
||||
# These both have :autosave enabled because accepts_nested_attributes_for is used on them.
|
||||
has_one :ship
|
||||
has_one :update_only_ship, :class_name => 'Ship'
|
||||
has_one :non_validated_ship, :class_name => 'Ship'
|
||||
has_many :birds
|
||||
has_many :birds_with_method_callbacks, :class_name => "Bird",
|
||||
|
@ -31,11 +32,14 @@ class Pirate < ActiveRecord::Base
|
|||
:after_add => proc {|p,b| p.ship_log << "after_adding_proc_bird_#{b.id || '<new>'}"},
|
||||
:before_remove => proc {|p,b| p.ship_log << "before_removing_proc_bird_#{b.id}"},
|
||||
:after_remove => proc {|p,b| p.ship_log << "after_removing_proc_bird_#{b.id}"}
|
||||
has_many :birds_with_reject_all_blank, :class_name => "Bird"
|
||||
|
||||
accepts_nested_attributes_for :parrots, :birds, :allow_destroy => true, :reject_if => proc { |attributes| attributes.empty? }
|
||||
accepts_nested_attributes_for :ship, :allow_destroy => true, :reject_if => proc { |attributes| attributes.empty? }
|
||||
accepts_nested_attributes_for :update_only_ship, :update_only => true
|
||||
accepts_nested_attributes_for :parrots_with_method_callbacks, :parrots_with_proc_callbacks,
|
||||
:birds_with_method_callbacks, :birds_with_proc_callbacks, :allow_destroy => true
|
||||
accepts_nested_attributes_for :birds_with_reject_all_blank, :reject_if => :all_blank
|
||||
|
||||
validates_presence_of :catchphrase
|
||||
|
||||
|
@ -47,6 +51,12 @@ class Pirate < ActiveRecord::Base
|
|||
attributes.delete('_reject_me_if_new').present? && new_record?
|
||||
end
|
||||
|
||||
attr_accessor :cancel_save_from_callback
|
||||
before_save :cancel_save_callback_method, :if => :cancel_save_from_callback
|
||||
def cancel_save_callback_method
|
||||
false
|
||||
end
|
||||
|
||||
private
|
||||
def log_before_add(record)
|
||||
log(record, "before_adding_method")
|
||||
|
|
11
vendor/rails/activerecord/test/models/ship.rb
vendored
11
vendor/rails/activerecord/test/models/ship.rb
vendored
|
@ -2,9 +2,18 @@ class Ship < ActiveRecord::Base
|
|||
self.record_timestamps = false
|
||||
|
||||
belongs_to :pirate
|
||||
has_many :parts, :class_name => 'ShipPart', :autosave => true
|
||||
belongs_to :update_only_pirate, :class_name => 'Pirate'
|
||||
has_many :parts, :class_name => 'ShipPart'
|
||||
|
||||
accepts_nested_attributes_for :parts, :allow_destroy => true
|
||||
accepts_nested_attributes_for :pirate, :allow_destroy => true, :reject_if => proc { |attributes| attributes.empty? }
|
||||
accepts_nested_attributes_for :update_only_pirate, :update_only => true
|
||||
|
||||
validates_presence_of :name
|
||||
|
||||
attr_accessor :cancel_save_from_callback
|
||||
before_save :cancel_save_callback_method, :if => :cancel_save_from_callback
|
||||
def cancel_save_callback_method
|
||||
false
|
||||
end
|
||||
end
|
||||
|
|
|
@ -1,5 +1,7 @@
|
|||
class ShipPart < ActiveRecord::Base
|
||||
belongs_to :ship
|
||||
|
||||
has_many :trinkets, :class_name => "Treasure", :as => :looter
|
||||
accepts_nested_attributes_for :trinkets, :allow_destroy => true
|
||||
|
||||
validates_presence_of :name
|
||||
end
|
3
vendor/rails/activerecord/test/models/zine.rb
vendored
Normal file
3
vendor/rails/activerecord/test/models/zine.rb
vendored
Normal file
|
@ -0,0 +1,3 @@
|
|||
class Zine < ActiveRecord::Base
|
||||
has_many :interests, :inverse_of => :zine
|
||||
end
|
41
vendor/rails/activerecord/test/schema/schema.rb
vendored
41
vendor/rails/activerecord/test/schema/schema.rb
vendored
|
@ -58,6 +58,7 @@ ActiveRecord::Schema.define do
|
|||
|
||||
create_table :birds, :force => true do |t|
|
||||
t.string :name
|
||||
t.string :color
|
||||
t.integer :pirate_id
|
||||
end
|
||||
|
||||
|
@ -174,6 +175,12 @@ ActiveRecord::Schema.define do
|
|||
|
||||
create_table :events, :force => true do |t|
|
||||
t.string :title, :limit => 5
|
||||
t.datetime :ends_on
|
||||
end
|
||||
|
||||
create_table :event_authors, :force => true do |t|
|
||||
t.integer :event_id
|
||||
t.integer :author_id
|
||||
end
|
||||
|
||||
create_table :funny_jokes, :force => true do |t|
|
||||
|
@ -185,6 +192,11 @@ ActiveRecord::Schema.define do
|
|||
t.string :info
|
||||
end
|
||||
|
||||
create_table :invoices, :force => true do |t|
|
||||
t.integer :balance
|
||||
t.datetime :updated_at
|
||||
end
|
||||
|
||||
create_table :items, :force => true do |t|
|
||||
t.column :name, :integer
|
||||
end
|
||||
|
@ -210,6 +222,11 @@ ActiveRecord::Schema.define do
|
|||
t.integer :version, :null => false, :default => 0
|
||||
end
|
||||
|
||||
create_table :line_items, :force => true do |t|
|
||||
t.integer :invoice_id
|
||||
t.integer :amount
|
||||
end
|
||||
|
||||
create_table :lock_without_defaults, :force => true do |t|
|
||||
t.column :lock_version, :integer
|
||||
end
|
||||
|
@ -479,6 +496,30 @@ ActiveRecord::Schema.define do
|
|||
end
|
||||
end
|
||||
|
||||
# NOTE - the following 4 tables are used by models that have :inverse_of options on the associations
|
||||
create_table :men, :force => true do |t|
|
||||
t.string :name
|
||||
end
|
||||
|
||||
create_table :faces, :force => true do |t|
|
||||
t.string :description
|
||||
t.integer :man_id
|
||||
t.integer :polymorphic_man_id
|
||||
t.string :polymorphic_man_type
|
||||
end
|
||||
|
||||
create_table :interests, :force => true do |t|
|
||||
t.string :topic
|
||||
t.integer :man_id
|
||||
t.integer :polymorphic_man_id
|
||||
t.string :polymorphic_man_type
|
||||
t.integer :zine_id
|
||||
end
|
||||
|
||||
create_table :zines, :force => true do |t|
|
||||
t.string :title
|
||||
end
|
||||
|
||||
except 'SQLite' do
|
||||
# fk_test_has_fk should be before fk_test_has_pk
|
||||
create_table :fk_test_has_fk, :force => true do |t|
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue