Update to Rails 2.3.8

This commit is contained in:
Jacques Distler 2010-05-25 12:45:45 -05:00
parent 6677b46cb4
commit f0635301aa
429 changed files with 17683 additions and 4047 deletions

View file

@ -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]

View file

@ -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

View file

@ -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"

View file

@ -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'

View file

@ -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

View file

@ -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
]

View file

@ -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) }

View file

@ -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

View file

@ -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

View file

@ -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

View file

@ -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

View file

@ -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

View file

@ -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

View file

@ -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)

View file

@ -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

View file

@ -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)

View file

@ -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"

View file

@ -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

View file

@ -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)

View 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

View file

@ -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)

View file

@ -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

View file

@ -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

View file

@ -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

View file

@ -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

View file

@ -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]

View file

@ -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.

View file

@ -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)

View file

@ -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

View file

@ -891,6 +891,7 @@ module ActiveRecord
instances.size == 1 ? instances.first : instances
end
private table_name
end
end

View file

@ -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

View file

@ -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'

View file

@ -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

View file

@ -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
# records 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 records 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

View file

@ -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

View file

@ -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

View file

@ -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

View file

@ -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

View file

@ -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

View file

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

View file

@ -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

View file

@ -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)

View file

@ -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

View file

@ -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

View file

@ -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

View 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

View file

@ -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"

View file

@ -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

View file

@ -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")

View file

@ -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

View file

@ -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

View file

@ -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

View file

@ -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

View file

@ -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

View file

@ -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)

View file

@ -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")

View file

@ -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

View file

@ -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

View file

@ -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

View file

@ -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

View file

@ -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

View file

@ -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

View file

@ -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"

View file

@ -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)

View 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

View file

@ -0,0 +1,11 @@
trusting:
description: trusting
man: gordon
weather_beaten:
description: weather beaten
man: steve
confused:
description: confused
polymorphic_man: gordon (Man)

View 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)

View file

@ -0,0 +1,5 @@
gordon:
name: Gordon
steve:
name: Steve

View file

@ -0,0 +1,5 @@
staying_in:
title: Staying in '08
going_out:
title: Outdoor Pursuits 2k+8

View file

@ -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

View file

@ -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

View file

@ -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

View file

@ -0,0 +1,5 @@
class EventAuthor < ActiveRecord::Base
belongs_to :author
belongs_to :event
end

View 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

View 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

View 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

View file

@ -0,0 +1,3 @@
class LineItem < ActiveRecord::Base
belongs_to :invoice, :touch => true
end

View 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

View file

@ -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

View file

@ -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")

View file

@ -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

View file

@ -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

View file

@ -0,0 +1,3 @@
class Zine < ActiveRecord::Base
has_many :interests, :inverse_of => :zine
end

View file

@ -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|