Merge branch 'master' into sprockets

This commit is contained in:
Joshua Peek 2011-03-30 20:56:05 -05:00
commit 5df076ad09
96 changed files with 1733 additions and 612 deletions

View File

@ -38,6 +38,9 @@ platforms :mri_19 do
end
platforms :ruby do
if ENV["RB_FSEVENT"]
gem 'rb-fsevent'
end
gem 'json'
gem 'yajl-ruby'
gem "nokogiri", ">= 1.4.4"

View File

@ -14,7 +14,7 @@ module AbstractController
# Override AbstractController::Base's process_action to run the
# process_action callbacks around the normal behavior.
def process_action(method_name, *args)
run_callbacks(:process_action, method_name) do
run_callbacks(:process_action, action_name) do
super
end
end

View File

@ -107,7 +107,7 @@ module ActionDispatch
if @options[:format] == false
@options.delete(:format)
path
elsif path.include?(":format") || path.end_with?('/')
elsif path.include?(":format") || path.end_with?('/') || path.match(/^\/?\*/)
path
else
"#{path}(.:format)"
@ -364,6 +364,13 @@ module ActionDispatch
# match 'path' => 'c#a', :defaults => { :format => 'jpg' }
#
# See <tt>Scoping#defaults</tt> for its scope equivalent.
#
# [:anchor]
# Boolean to anchor a #match pattern. Default is true. When set to
# false, the pattern matches any request prefixed with the given path.
#
# # Matches any request starting with 'path'
# match 'path' => 'c#a', :anchor => false
def match(path, options=nil)
mapping = Mapping.new(@set, @scope, path, options || {})
app, conditions, requirements, defaults, as, anchor = mapping.to_route

View File

@ -46,6 +46,13 @@ module ActionDispatch
mapper.match '/one/two/', :to => 'posts#index', :as => :main
assert_equal '/one/two(.:format)', fakeset.conditions.first[:path_info]
end
def test_map_wildcard
fakeset = FakeSet.new
mapper = Mapper.new fakeset
mapper.match '/*path', :to => 'pages#show', :as => :page
assert_equal '/*path', fakeset.conditions.first[:path_info]
end
end
end
end

View File

@ -505,6 +505,21 @@ class FilterTest < ActionController::TestCase
end
end
class ImplicitActionsController < ActionController::Base
before_filter :find_only, :only => :edit
before_filter :find_except, :except => :edit
private
def find_only
@only = 'Only'
end
def find_except
@except = 'Except'
end
end
def test_sweeper_should_not_block_rendering
response = test_process(SweeperTestController)
assert_equal 'hello world', response.body
@ -783,6 +798,18 @@ class FilterTest < ActionController::TestCase
assert_equal("I rescued this: #<FilterTest::ErrorToRescue: Something made the bad noise.>", response.body)
end
def test_filters_obey_only_and_except_for_implicit_actions
test_process(ImplicitActionsController, 'show')
assert_equal 'Except', assigns(:except)
assert_nil assigns(:only)
assert_equal 'show', response.body
test_process(ImplicitActionsController, 'edit')
assert_equal 'Only', assigns(:only)
assert_nil assigns(:except)
assert_equal 'edit', response.body
end
private
def test_process(controller, action = "show")
@controller = controller.is_a?(Class) ? controller.new : controller

View File

@ -0,0 +1 @@
edit

View File

@ -0,0 +1 @@
show

View File

@ -106,8 +106,14 @@ module ActiveModel
if block_given?
sing.send :define_method, name, &block
else
value = value.to_s if value
sing.send(:define_method, name) { value && value.dup }
if name =~ /^[a-zA-Z_]\w*[!?=]?$/
sing.class_eval <<-eorb, __FILE__, __LINE__ + 1
def #{name}; #{value.nil? ? 'nil' : value.to_s.inspect}; end
eorb
else
value = value.to_s if value
sing.send(:define_method, name) { value }
end
end
end

View File

@ -9,10 +9,6 @@ class ModelWithAttributes
define_method(:bar) do
'original bar'
end
define_method(:zomg) do
'original zomg'
end
end
def attributes
@ -102,13 +98,6 @@ class AttributeMethodsTest < ActiveModel::TestCase
assert_equal "value of foo bar", ModelWithAttributesWithSpaces.new.send(:'foo bar')
end
def test_defined_methods_always_return_duped_string
ModelWithAttributes.define_attr_method(:zomg, 'lol')
assert_equal 'lol', ModelWithAttributes.zomg
ModelWithAttributes.zomg << 'bbq'
assert_equal 'lol', ModelWithAttributes.zomg
end
test '#define_attr_method generates attribute method' do
ModelWithAttributes.define_attr_method(:bar, 'bar')

View File

@ -1,5 +1,11 @@
*Rails 3.1.0 (unreleased)*
* Associations with a :through option can now use *any* association as the
through or source association, including other associations which have a
:through option and has_and_belongs_to_many associations
[Jon Leighton]
* The configuration for the current database connection is now accessible via
ActiveRecord::Base.connection_config. [fxn]

View File

@ -52,14 +52,6 @@ module ActiveRecord
end
end
class HasManyThroughSourceAssociationMacroError < ActiveRecordError #:nodoc:
def initialize(reflection)
through_reflection = reflection.through_reflection
source_reflection = reflection.source_reflection
super("Invalid source reflection macro :#{source_reflection.macro}#{" :through" if source_reflection.options[:through]} for has_many #{reflection.name.inspect}, :through => #{through_reflection.name.inspect}. Use :source to specify the source reflection.")
end
end
class HasManyThroughCantAssociateThroughHasOneOrManyReflection < ActiveRecordError #:nodoc:
def initialize(owner, reflection)
super("Cannot modify association '#{owner.class.name}##{reflection.name}' because the source reflection class '#{reflection.source_reflection.class_name}' is associated to '#{reflection.through_reflection.class_name}' via :#{reflection.source_reflection.macro}.")
@ -78,6 +70,12 @@ module ActiveRecord
end
end
class HasManyThroughNestedAssociationsAreReadonly < ActiveRecordError #:nodoc
def initialize(owner, reflection)
super("Cannot modify association '#{owner.class.name}##{reflection.name}' because it goes through more than one other association.")
end
end
class HasAndBelongsToManyAssociationWithPrimaryKeyError < ActiveRecordError #:nodoc:
def initialize(reflection)
super("Primary key is not allowed in a has_and_belongs_to_many join table (#{reflection.options[:join_table]}).")
@ -142,8 +140,11 @@ module ActiveRecord
autoload :HasAndBelongsToMany, 'active_record/associations/builder/has_and_belongs_to_many'
end
autoload :Preloader, 'active_record/associations/preloader'
autoload :JoinDependency, 'active_record/associations/join_dependency'
autoload :Preloader, 'active_record/associations/preloader'
autoload :JoinDependency, 'active_record/associations/join_dependency'
autoload :AssociationScope, 'active_record/associations/association_scope'
autoload :AliasTracker, 'active_record/associations/alias_tracker'
autoload :JoinHelper, 'active_record/associations/join_helper'
# Clears out the association cache.
def clear_association_cache #:nodoc:
@ -548,6 +549,49 @@ module ActiveRecord
# belongs_to :tag, :inverse_of => :taggings
# end
#
# === Nested Associations
#
# You can actually specify *any* association with the <tt>:through</tt> option, including an
# association which has a <tt>:through</tt> option itself. For example:
#
# class Author < ActiveRecord::Base
# has_many :posts
# has_many :comments, :through => :posts
# has_many :commenters, :through => :comments
# end
#
# class Post < ActiveRecord::Base
# has_many :comments
# end
#
# class Comment < ActiveRecord::Base
# belongs_to :commenter
# end
#
# @author = Author.first
# @author.commenters # => People who commented on posts written by the author
#
# An equivalent way of setting up this association this would be:
#
# class Author < ActiveRecord::Base
# has_many :posts
# has_many :commenters, :through => :posts
# end
#
# class Post < ActiveRecord::Base
# has_many :comments
# has_many :commenters, :through => :comments
# end
#
# class Comment < ActiveRecord::Base
# belongs_to :commenter
# end
#
# When using nested association, you will not be able to modify the association because there
# is not enough information to know what modification to make. For example, if you tried to
# add a <tt>Commenter</tt> in the example above, there would be no way to tell how to set up the
# intermediate <tt>Post</tt> and <tt>Comment</tt> objects.
#
# === Polymorphic Associations
#
# Polymorphic associations on models are not restricted on what types of models they
@ -1068,10 +1112,10 @@ module ActiveRecord
# [:as]
# Specifies a polymorphic interface (See <tt>belongs_to</tt>).
# [:through]
# Specifies a join model through which to perform the query. Options for <tt>:class_name</tt>,
# Specifies an association through which to perform the query. This can be any other type
# of association, including other <tt>:through</tt> associations. Options for <tt>:class_name</tt>,
# <tt>:primary_key</tt> and <tt>:foreign_key</tt> are ignored, as the association uses the
# source reflection. You can only use a <tt>:through</tt> query through a <tt>belongs_to</tt>,
# <tt>has_one</tt> or <tt>has_many</tt> association on the join model.
# source reflection.
#
# If the association on the join model is a +belongs_to+, the collection can be modified
# and the records on the <tt>:through</tt> model will be automatically created and removed
@ -1198,10 +1242,10 @@ module ActiveRecord
# you want to do a join but not include the joined columns. Do not forget to include the
# primary and foreign keys, otherwise it will raise an error.
# [:through]
# Specifies a Join Model through which to perform the query. Options for <tt>:class_name</tt>
# and <tt>:foreign_key</tt> are ignored, as the association uses the source reflection. You
# can only use a <tt>:through</tt> query through a <tt>has_one</tt> or <tt>belongs_to</tt>
# association on the join model.
# Specifies a Join Model through which to perform the query. Options for <tt>:class_name</tt>,
# <tt>:primary_key</tt>, and <tt>:foreign_key</tt> are ignored, as the association uses the
# source reflection. You can only use a <tt>:through</tt> query through a <tt>has_one</tt>
# or <tt>belongs_to</tt> association on the join model.
# [:source]
# Specifies the source association name used by <tt>has_one :through</tt> queries.
# Only use it if the name cannot be inferred from the association.

View File

@ -0,0 +1,85 @@
require 'active_support/core_ext/string/conversions'
module ActiveRecord
module Associations
# Keeps track of table aliases for ActiveRecord::Associations::ClassMethods::JoinDependency and
# ActiveRecord::Associations::ThroughAssociationScope
class AliasTracker # :nodoc:
attr_reader :aliases, :table_joins
# table_joins is an array of arel joins which might conflict with the aliases we assign here
def initialize(table_joins = [])
@aliases = Hash.new
@table_joins = table_joins
end
def aliased_table_for(table_name, aliased_name = nil)
table_alias = aliased_name_for(table_name, aliased_name)
if table_alias == table_name
Arel::Table.new(table_name)
else
Arel::Table.new(table_name).alias(table_alias)
end
end
def aliased_name_for(table_name, aliased_name = nil)
aliased_name ||= table_name
initialize_count_for(table_name) if aliases[table_name].nil?
if aliases[table_name].zero?
# If it's zero, we can have our table_name
aliases[table_name] = 1
table_name
else
# Otherwise, we need to use an alias
aliased_name = connection.table_alias_for(aliased_name)
initialize_count_for(aliased_name) if aliases[aliased_name].nil?
# Update the count
aliases[aliased_name] += 1
if aliases[aliased_name] > 1
"#{truncate(aliased_name)}_#{aliases[aliased_name]}"
else
aliased_name
end
end
end
def pluralize(table_name)
ActiveRecord::Base.pluralize_table_names ? table_name.to_s.pluralize : table_name
end
private
def initialize_count_for(name)
aliases[name] = 0
unless Arel::Table === table_joins
# quoted_name should be downcased as some database adapters (Oracle) return quoted name in uppercase
quoted_name = connection.quote_table_name(name).downcase
aliases[name] += table_joins.map { |join|
# Table names + table aliases
join.left.downcase.scan(
/join(?:\s+\w+)?\s+(\S+\s+)?#{quoted_name}\son/
).size
}.sum
end
aliases[name]
end
def truncate(name)
name[0..connection.table_alias_length-3]
end
def connection
ActiveRecord::Base.connection
end
end
end
end

View File

@ -93,23 +93,9 @@ module ActiveRecord
# by scope.scoping { ... } or with_scope { ... } etc, which affects the scope which
# actually gets built.
def construct_scope
@association_scope = association_scope if klass
end
def association_scope
scope = klass.unscoped
scope = scope.create_with(creation_attributes)
scope = scope.apply_finder_options(options.slice(:readonly, :include))
scope = scope.where(interpolate(options[:conditions]))
if select = select_value
scope = scope.select(select)
if klass
@association_scope = AssociationScope.new(self).scope
end
scope = scope.extending(*Array.wrap(options[:extend]))
scope.where(construct_owner_conditions)
end
def aliased_table
klass.arel_table
end
# Set the inverse association, if possible
@ -174,42 +160,24 @@ module ActiveRecord
end
end
def select_value
options[:select]
end
# Implemented by (some) subclasses
def creation_attributes
{ }
end
# Returns a hash linking the owner to the association represented by the reflection
def construct_owner_attributes(reflection = reflection)
attributes = {}
if reflection.macro == :belongs_to
attributes[reflection.association_primary_key] = owner[reflection.foreign_key]
else
if [:has_one, :has_many].include?(reflection.macro) && !options[:through]
attributes[reflection.foreign_key] = owner[reflection.active_record_primary_key]
if reflection.options[:as]
attributes[reflection.type] = owner.class.base_class.name
end
end
attributes
end
# Builds an array of arel nodes from the owner attributes hash
def construct_owner_conditions(table = aliased_table, reflection = reflection)
conditions = construct_owner_attributes(reflection).map do |attr, value|
table[attr].eq(value)
end
table.create_and(conditions)
attributes
end
# Sets the owner attributes on the given record
def set_owner_attributes(record)
if owner.persisted?
construct_owner_attributes.each { |key, value| record[key] = value }
creation_attributes.each { |key, value| record[key] = value }
end
end

View File

@ -0,0 +1,120 @@
module ActiveRecord
module Associations
class AssociationScope #:nodoc:
include JoinHelper
attr_reader :association, :alias_tracker
delegate :klass, :owner, :reflection, :interpolate, :to => :association
delegate :chain, :conditions, :options, :source_options, :active_record, :to => :reflection
def initialize(association)
@association = association
@alias_tracker = AliasTracker.new
end
def scope
scope = klass.unscoped
scope = scope.extending(*Array.wrap(options[:extend]))
# It's okay to just apply all these like this. The options will only be present if the
# association supports that option; this is enforced by the association builder.
scope = scope.apply_finder_options(options.slice(
:readonly, :include, :order, :limit, :joins, :group, :having, :offset))
if options[:through] && !options[:include]
scope = scope.includes(source_options[:include])
end
if select = select_value
scope = scope.select(select)
end
add_constraints(scope)
end
private
def select_value
select_value = options[:select]
if reflection.collection?
select_value ||= options[:uniq] && "DISTINCT #{reflection.quoted_table_name}.*"
end
if reflection.macro == :has_and_belongs_to_many
select_value ||= reflection.klass.arel_table[Arel.star]
end
select_value
end
def add_constraints(scope)
tables = construct_tables
chain.each_with_index do |reflection, i|
table, foreign_table = tables.shift, tables.first
if reflection.source_macro == :has_and_belongs_to_many
join_table = tables.shift
scope = scope.joins(join(
join_table,
table[reflection.active_record_primary_key].
eq(join_table[reflection.association_foreign_key])
))
table, foreign_table = join_table, tables.first
end
if reflection.source_macro == :belongs_to
key = reflection.association_primary_key
foreign_key = reflection.foreign_key
else
key = reflection.foreign_key
foreign_key = reflection.active_record_primary_key
end
if reflection == chain.last
scope = scope.where(table[key].eq(owner[foreign_key]))
conditions[i].each do |condition|
if options[:through] && condition.is_a?(Hash)
condition = { table.name => condition }
end
scope = scope.where(interpolate(condition))
end
else
constraint = table[key].eq(foreign_table[foreign_key])
join = join(foreign_table, constraint)
scope = scope.joins(join)
unless conditions[i].empty?
scope = scope.where(sanitize(conditions[i], table))
end
end
end
scope
end
def alias_suffix
reflection.name
end
def table_name_for(reflection)
if reflection == self.reflection
# If this is a polymorphic belongs_to, we want to get the klass from the
# association because it depends on the polymorphic_type attribute of
# the owner
klass.table_name
else
reflection.table_name
end
end
end
end
end

View File

@ -7,24 +7,24 @@ module ActiveRecord::Associations::Builder
def build
reflection = super
check_validity(reflection)
redefine_destroy
define_after_destroy_method
reflection
end
private
def redefine_destroy
# Don't use a before_destroy callback since users' before_destroy
# callbacks will be executed after the association is wiped out.
def define_after_destroy_method
name = self.name
model.send(:include, Module.new {
class_eval <<-RUBY, __FILE__, __LINE__ + 1
def destroy # def destroy
super # super
#{name}.clear # posts.clear
end # end
RUBY
})
model.send(:class_eval, <<-eoruby, __FILE__, __LINE__ + 1)
def #{after_destroy_method_name}
association(#{name.to_sym.inspect}).delete_all
end
eoruby
model.after_destroy after_destroy_method_name
end
def after_destroy_method_name
"has_and_belongs_to_many_after_destroy_for_#{name}"
end
# TODO: These checks should probably be moved into the Reflection, and we should not be

View File

@ -331,11 +331,6 @@ module ActiveRecord
@scopes_cache[method][args] ||= scoped.readonly(nil).send(method, *args)
end
def association_scope
options = reflection.options.slice(:order, :limit, :joins, :group, :having, :offset)
super.apply_finder_options(options)
end
def load_target
if find_target?
targets = []
@ -373,14 +368,6 @@ module ActiveRecord
private
def select_value
super || uniq_select_value
end
def uniq_select_value
options[:uniq] && "DISTINCT #{reflection.quoted_table_name}.*"
end
def custom_counter_sql
if options[:counter_sql]
interpolate(options[:counter_sql])

View File

@ -26,10 +26,6 @@ module ActiveRecord
record
end
def association_scope
super.joins(construct_joins)
end
private
def count_records
@ -48,24 +44,6 @@ module ActiveRecord
end
end
def construct_joins
right = join_table
left = reflection.klass.arel_table
condition = left[reflection.klass.primary_key].eq(
right[reflection.association_foreign_key])
right.create_join(right, right.create_on(condition))
end
def construct_owner_conditions
super(join_table)
end
def select_value
super || reflection.klass.arel_table[Arel.star]
end
def invertible_for?(record)
false
end

View File

@ -94,8 +94,6 @@ module ActiveRecord
end
end
end
alias creation_attributes construct_owner_attributes
end
end
end

View File

@ -34,7 +34,9 @@ module ActiveRecord
end
def insert_record(record, validate = true)
ensure_not_nested
return if record.new_record? && !record.save(:validate => validate)
through_record(record).save!
update_counter(1)
record
@ -59,6 +61,8 @@ module ActiveRecord
end
def build_record(attributes)
ensure_not_nested
record = super(attributes)
inverse = source_reflection.inverse_of
@ -93,6 +97,8 @@ module ActiveRecord
end
def delete_records(records, method)
ensure_not_nested
through = owner.association(through_reflection.name)
scope = through.scoped.where(construct_join_attributes(*records))

View File

@ -39,14 +39,8 @@ module ActiveRecord
end
end
def association_scope
super.order(options[:order])
end
private
alias creation_attributes construct_owner_attributes
# The reason that the save param for replace is false, if for create (not just build),
# is because the setting of the foreign keys is actually handled by the scoping when
# the record is instantiated, and so they are set straight away and do not need to be

View File

@ -12,6 +12,8 @@ module ActiveRecord
private
def create_through_record(record)
ensure_not_nested
through_proxy = owner.association(through_reflection.name)
through_record = through_proxy.send(:load_target)

View File

@ -5,18 +5,16 @@ module ActiveRecord
autoload :JoinBase, 'active_record/associations/join_dependency/join_base'
autoload :JoinAssociation, 'active_record/associations/join_dependency/join_association'
attr_reader :join_parts, :reflections, :table_aliases, :active_record
attr_reader :join_parts, :reflections, :alias_tracker, :active_record
def initialize(base, associations, joins)
@active_record = base
@table_joins = joins
@join_parts = [JoinBase.new(base)]
@associations = {}
@reflections = []
@table_aliases = Hash.new do |h,name|
h[name] = count_aliases_from_table_joins(name.downcase)
end
@table_aliases[base.table_name] = 1
@active_record = base
@table_joins = joins
@join_parts = [JoinBase.new(base)]
@associations = {}
@reflections = []
@alias_tracker = AliasTracker.new(joins)
@alias_tracker.aliased_name_for(base.table_name) # Updates the count for base.table_name to 1
build(associations)
end
@ -45,20 +43,6 @@ module ActiveRecord
}.flatten
end
def count_aliases_from_table_joins(name)
return 0 if Arel::Table === @table_joins
# quoted_name should be downcased as some database adapters (Oracle) return quoted name in uppercase
quoted_name = active_record.connection.quote_table_name(name).downcase
@table_joins.map { |join|
# Table names + table aliases
join.left.downcase.scan(
/join(?:\s+\w+)?\s+(\S+\s+)?#{quoted_name}\son/
).size
}.sum
end
def instantiate(rows)
primary_key = join_base.aliased_primary_key
parents = {}

View File

@ -2,6 +2,8 @@ module ActiveRecord
module Associations
class JoinDependency # :nodoc:
class JoinAssociation < JoinPart # :nodoc:
include JoinHelper
# The reflection of the association represented
attr_reader :reflection
@ -18,10 +20,15 @@ module ActiveRecord
attr_accessor :join_type
# These implement abstract methods from the superclass
attr_reader :aliased_prefix, :aliased_table_name
attr_reader :aliased_prefix
delegate :options, :through_reflection, :source_reflection, :to => :reflection
attr_reader :tables
delegate :options, :through_reflection, :source_reflection, :chain, :to => :reflection
delegate :table, :table_name, :to => :parent, :prefix => :parent
delegate :alias_tracker, :to => :join_dependency
alias :alias_suffix :parent_table_name
def initialize(reflection, join_dependency, parent = nil)
reflection.check_validity!
@ -37,14 +44,7 @@ module ActiveRecord
@parent = parent
@join_type = Arel::InnerJoin
@aliased_prefix = "t#{ join_dependency.join_parts.size }"
# This must be done eagerly upon initialisation because the alias which is produced
# depends on the state of the join dependency, but we want it to work the same way
# every time.
allocate_aliases
@table = Arel::Table.new(
table_name, :as => aliased_table_name, :engine => arel_engine
)
@tables = construct_tables.reverse
end
def ==(other)
@ -60,7 +60,55 @@ module ActiveRecord
end
def join_to(relation)
send("join_#{reflection.macro}_to", relation)
tables = @tables.dup
foreign_table = parent_table
# The chain starts with the target table, but we want to end with it here (makes
# more sense in this context), so we reverse
chain.reverse.each_with_index do |reflection, i|
table = tables.shift
case reflection.source_macro
when :belongs_to
key = reflection.association_primary_key
foreign_key = reflection.foreign_key
when :has_and_belongs_to_many
# Join the join table first...
relation.from(join(
table,
table[reflection.foreign_key].
eq(foreign_table[reflection.active_record_primary_key])
))
foreign_table, table = table, tables.shift
key = reflection.association_primary_key
foreign_key = reflection.association_foreign_key
else
key = reflection.foreign_key
foreign_key = reflection.active_record_primary_key
end
constraint = table[key].eq(foreign_table[foreign_key])
if reflection.klass.finder_needs_type_condition?
constraint = table.create_and([
constraint,
reflection.klass.send(:type_condition, table)
])
end
relation.from(join(table, constraint))
unless conditions[i].empty?
relation.where(sanitize(conditions[i], table))
end
# The current table in this iteration becomes the foreign table in the next
foreign_table = table
end
relation
end
def join_relation(joining_relation)
@ -68,211 +116,28 @@ module ActiveRecord
joining_relation.joins(self)
end
attr_reader :table
# More semantic name given we are talking about associations
alias_method :target_table, :table
protected
def aliased_table_name_for(name, suffix = nil)
aliases = @join_dependency.table_aliases
if aliases[name] != 0 # We need an alias
connection = active_record.connection
name = connection.table_alias_for "#{pluralize(reflection.name)}_#{parent_table_name}#{suffix}"
aliases[name] += 1
name = name[0, connection.table_alias_length-3] + "_#{aliases[name]}" if aliases[name] > 1
else
aliases[name] += 1
end
name
def table
tables.last
end
def pluralize(table_name)
ActiveRecord::Base.pluralize_table_names ? table_name.to_s.pluralize : table_name
def aliased_table_name
table.table_alias || table.name
end
def conditions
@conditions ||= reflection.conditions.reverse
end
private
def allocate_aliases
@aliased_table_name = aliased_table_name_for(table_name)
if reflection.macro == :has_and_belongs_to_many
@aliased_join_table_name = aliased_table_name_for(reflection.options[:join_table], "_join")
elsif [:has_many, :has_one].include?(reflection.macro) && reflection.options[:through]
@aliased_join_table_name = aliased_table_name_for(reflection.through_reflection.klass.table_name, "_join")
end
end
def process_conditions(conditions, table_name)
def interpolate(conditions)
if conditions.respond_to?(:to_proc)
conditions = instance_eval(&conditions)
end
Arel.sql(sanitize_sql(conditions, table_name))
end
def sanitize_sql(condition, table_name)
active_record.send(:sanitize_sql, condition, table_name)
end
def join_target_table(relation, condition)
conditions = [condition]
# If the target table is an STI model then we must be sure to only include records of
# its type and its sub-types.
unless active_record.descends_from_active_record?
sti_column = target_table[active_record.inheritance_column]
subclasses = active_record.descendants
sti_condition = sti_column.eq(active_record.sti_name)
conditions << subclasses.inject(sti_condition) { |attr,subclass|
attr.or(sti_column.eq(subclass.sti_name))
}
end
# If the reflection has conditions, add them
if options[:conditions]
conditions << process_conditions(options[:conditions], aliased_table_name)
end
ands = relation.create_and(conditions)
join = relation.create_join(
target_table,
relation.create_on(ands),
join_type)
relation.from join
end
def join_has_and_belongs_to_many_to(relation)
join_table = Arel::Table.new(
options[:join_table]
).alias(@aliased_join_table_name)
fk = options[:foreign_key] || reflection.active_record.to_s.foreign_key
klass_fk = options[:association_foreign_key] || reflection.klass.to_s.foreign_key
relation = relation.join(join_table, join_type)
relation = relation.on(
join_table[fk].
eq(parent_table[reflection.active_record.primary_key])
)
join_target_table(
relation,
target_table[reflection.klass.primary_key].
eq(join_table[klass_fk])
)
end
def join_has_many_to(relation)
if reflection.options[:through]
join_has_many_through_to(relation)
elsif reflection.options[:as]
join_has_many_polymorphic_to(relation)
instance_eval(&conditions)
else
foreign_key = options[:foreign_key] || reflection.active_record.name.foreign_key
primary_key = options[:primary_key] || parent.primary_key
join_target_table(
relation,
target_table[foreign_key].
eq(parent_table[primary_key])
)
conditions
end
end
alias :join_has_one_to :join_has_many_to
def join_has_many_through_to(relation)
join_table = Arel::Table.new(
through_reflection.klass.table_name
).alias @aliased_join_table_name
jt_conditions = []
first_key = second_key = nil
if through_reflection.macro == :belongs_to
jt_primary_key = through_reflection.foreign_key
jt_foreign_key = through_reflection.association_primary_key
else
jt_primary_key = through_reflection.active_record_primary_key
jt_foreign_key = through_reflection.foreign_key
if through_reflection.options[:as] # has_many :through against a polymorphic join
jt_conditions <<
join_table["#{through_reflection.options[:as]}_type"].
eq(parent.active_record.base_class.name)
end
end
case source_reflection.macro
when :has_many
second_key = options[:foreign_key] || primary_key
if source_reflection.options[:as]
first_key = "#{source_reflection.options[:as]}_id"
else
first_key = through_reflection.klass.base_class.to_s.foreign_key
end
unless through_reflection.klass.descends_from_active_record?
jt_conditions <<
join_table[through_reflection.active_record.inheritance_column].
eq(through_reflection.klass.sti_name)
end
when :belongs_to
first_key = primary_key
if reflection.options[:source_type]
second_key = source_reflection.association_foreign_key
jt_conditions <<
join_table[reflection.source_reflection.foreign_type].
eq(reflection.options[:source_type])
else
second_key = source_reflection.foreign_key
end
end
jt_conditions <<
parent_table[jt_primary_key].
eq(join_table[jt_foreign_key])
if through_reflection.options[:conditions]
jt_conditions << process_conditions(through_reflection.options[:conditions], aliased_table_name)
end
relation = relation.join(join_table, join_type).on(*jt_conditions)
join_target_table(
relation,
target_table[first_key].eq(join_table[second_key])
)
end
def join_has_many_polymorphic_to(relation)
join_target_table(
relation,
target_table["#{reflection.options[:as]}_id"].
eq(parent_table[parent.primary_key]).and(
target_table["#{reflection.options[:as]}_type"].
eq(parent.active_record.base_class.name))
)
end
def join_belongs_to_to(relation)
foreign_key = options[:foreign_key] || reflection.foreign_key
primary_key = options[:primary_key] || reflection.klass.primary_key
join_target_table(
relation,
target_table[primary_key].eq(parent_table[foreign_key])
)
end
end
end
end

View File

@ -0,0 +1,56 @@
module ActiveRecord
module Associations
# Helper class module which gets mixed into JoinDependency::JoinAssociation and AssociationScope
module JoinHelper #:nodoc:
def join_type
Arel::InnerJoin
end
private
def construct_tables
tables = []
chain.each do |reflection|
tables << alias_tracker.aliased_table_for(
table_name_for(reflection),
table_alias_for(reflection, reflection != self.reflection)
)
if reflection.source_macro == :has_and_belongs_to_many
tables << alias_tracker.aliased_table_for(
(reflection.source_reflection || reflection).options[:join_table],
table_alias_for(reflection, true)
)
end
end
tables
end
def table_name_for(reflection)
reflection.table_name
end
def table_alias_for(reflection, join = false)
name = alias_tracker.pluralize(reflection.name)
name << "_#{alias_suffix}"
name << "_join" if join
name
end
def join(table, constraint)
table.create_join(table, table.create_on(constraint), join_type)
end
def sanitize(conditions, table)
conditions = conditions.map do |condition|
condition = active_record.send(:sanitize_sql, interpolate(condition), table.table_alias || table.name)
condition = Arel.sql(condition) unless condition.is_a?(Arel::Node)
condition
end
conditions.length == 1 ? conditions.first : Arel::Nodes::And.new(conditions)
end
end
end
end

View File

@ -19,8 +19,9 @@ module ActiveRecord
source_reflection.name, options
).run
through_records.each do |owner, owner_through_records|
owner_through_records.map! { |r| r.send(source_reflection.name) }.flatten!
through_records.each do |owner, records|
records.map! { |r| r.send(source_reflection.name) }.flatten!
records.compact!
end
end

View File

@ -3,79 +3,24 @@ module ActiveRecord
module Associations
module ThroughAssociation #:nodoc:
delegate :source_options, :through_options, :source_reflection, :through_reflection, :to => :reflection
delegate :source_reflection, :through_reflection, :chain, :to => :reflection
protected
# We merge in these scopes for two reasons:
#
# 1. To get the default_scope conditions for any of the other reflections in the chain
# 2. To get the type conditions for any STI models in the chain
def target_scope
super.merge(through_reflection.klass.scoped)
end
def association_scope
scope = super.joins(construct_joins)
scope = add_conditions(scope)
unless options[:include]
scope = scope.includes(source_options[:include])
scope = super
chain[1..-1].each do |reflection|
scope = scope.merge(reflection.klass.scoped)
end
scope
end
private
# This scope affects the creation of the associated records (not the join records). At the
# moment we only support creating on a :through association when the source reflection is a
# belongs_to. Thus it's not necessary to set a foreign key on the associated record(s), so
# this scope has can legitimately be empty.
def creation_attributes
{ }
end
def aliased_through_table
name = through_reflection.table_name
reflection.table_name == name ?
through_reflection.klass.arel_table.alias(name + "_join") :
through_reflection.klass.arel_table
end
def construct_owner_conditions
super(aliased_through_table, through_reflection)
end
def construct_joins
right = aliased_through_table
left = reflection.klass.arel_table
conditions = []
if source_reflection.macro == :belongs_to
reflection_primary_key = source_reflection.association_primary_key
source_primary_key = source_reflection.foreign_key
if options[:source_type]
column = source_reflection.foreign_type
conditions <<
right[column].eq(options[:source_type])
end
else
reflection_primary_key = source_reflection.foreign_key
source_primary_key = source_reflection.active_record_primary_key
if source_options[:as]
column = "#{source_options[:as]}_type"
conditions <<
left[column].eq(through_reflection.klass.name)
end
end
conditions <<
left[reflection_primary_key].eq(right[source_primary_key])
right.create_join(
right,
right.create_on(right.create_and(conditions)))
end
# Construct attributes for :through pointing to owner and associate. This is used by the
# methods which create and delete records on the association.
#
@ -112,37 +57,8 @@ module ActiveRecord
end
end
# The reason that we are operating directly on the scope here (rather than passing
# back some arel conditions to be added to the scope) is because scope.where([x, y])
# has a different meaning to scope.where(x).where(y) - the first version might
# perform some substitution if x is a string.
def add_conditions(scope)
unless through_reflection.klass.descends_from_active_record?
scope = scope.where(through_reflection.klass.send(:type_condition))
end
scope = scope.where(interpolate(source_options[:conditions]))
scope.where(through_conditions)
end
# If there is a hash of conditions then we make sure the keys are scoped to the
# through table name if left ambiguous.
def through_conditions
conditions = interpolate(through_options[:conditions])
if conditions.is_a?(Hash)
Hash[conditions.map { |key, value|
unless value.is_a?(Hash) || key.to_s.include?('.')
key = aliased_through_table.name + '.' + key.to_s
end
[key, value]
}]
else
conditions
end
end
# Note: this does not capture all cases, for example it would be crazy to try to
# properly support stale-checking for nested associations.
def stale_state
if through_reflection.macro == :belongs_to
owner[through_reflection.foreign_key].to_s
@ -153,6 +69,12 @@ module ActiveRecord
through_reflection.macro == :belongs_to &&
!owner[through_reflection.foreign_key].nil?
end
def ensure_not_nested
if reflection.nested?
raise HasManyThroughNestedAssociationsAreReadonly.new(owner, reflection)
end
end
end
end
end

View File

@ -70,7 +70,14 @@ module ActiveRecord
if cache_attribute?(attr_name)
access_code = "@attributes_cache['#{attr_name}'] ||= (#{access_code})"
end
generated_attribute_methods.module_eval("def _#{symbol}; #{access_code}; end; alias #{symbol} _#{symbol}", __FILE__, __LINE__)
if symbol =~ /^[a-zA-Z_]\w*[!?=]?$/
generated_attribute_methods.module_eval("def _#{symbol}; #{access_code}; end; alias #{symbol} _#{symbol}", __FILE__, __LINE__)
else
generated_attribute_methods.module_eval do
define_method("_#{symbol}") { eval(access_code) }
alias_method(symbol, "_#{symbol}")
end
end
end
end

View File

@ -21,9 +21,9 @@ module ActiveRecord
def define_method_attribute(attr_name)
if create_time_zone_conversion_attribute?(attr_name, columns_hash[attr_name])
method_body, line = <<-EOV, __LINE__ + 1
def _#{attr_name}(reload = false)
def _#{attr_name}
cached = @attributes_cache['#{attr_name}']
return cached if cached && !reload
return cached if cached
time = _read_attribute('#{attr_name}')
@attributes_cache['#{attr_name}'] = time.acts_like?(:time) ? time.in_time_zone : time
end
@ -41,12 +41,13 @@ module ActiveRecord
if create_time_zone_conversion_attribute?(attr_name, columns_hash[attr_name])
method_body, line = <<-EOV, __LINE__ + 1
def #{attr_name}=(original_time)
time = original_time.dup unless original_time.nil?
time = original_time
unless time.acts_like?(:time)
time = time.is_a?(String) ? Time.zone.parse(time) : time.to_time rescue time
end
time = time.in_time_zone rescue nil if time
write_attribute(:#{attr_name}, (time || original_time))
write_attribute(:#{attr_name}, original_time)
@attributes_cache["#{attr_name}"] = time
end
EOV
generated_attribute_methods.module_eval(method_body, __FILE__, line)

View File

@ -10,7 +10,13 @@ module ActiveRecord
module ClassMethods
protected
def define_method_attribute=(attr_name)
generated_attribute_methods.module_eval("def #{attr_name}=(new_value); write_attribute('#{attr_name}', new_value); end", __FILE__, __LINE__)
if attr_name =~ /^[a-zA-Z_]\w*[!?=]?$/
generated_attribute_methods.module_eval("def #{attr_name}=(new_value); write_attribute('#{attr_name}', new_value); end", __FILE__, __LINE__)
else
generated_attribute_methods.send(:define_method, "#{attr_name}=") do |new_value|
write_attribute(attr_name, new_value)
end
end
end
end

View File

@ -973,8 +973,8 @@ module ActiveRecord #:nodoc:
relation
end
def type_condition
sti_column = arel_table[inheritance_column.to_sym]
def type_condition(table = arel_table)
sti_column = table[inheritance_column.to_sym]
sti_names = ([self] + descendants).map { |model| model.sti_name }
sti_column.in(sti_names)
@ -995,7 +995,7 @@ module ActiveRecord #:nodoc:
if parent < ActiveRecord::Base && !parent.abstract_class?
contained = parent.table_name
contained = contained.singularize if parent.pluralize_table_names
contained << '_'
contained += '_'
end
"#{full_table_name_prefix}#{contained}#{undecorated_table_name(name)}#{table_name_suffix}"
else

View File

@ -237,7 +237,6 @@ module ActiveRecord
# add_limit_offset!('SELECT * FROM suppliers', {:limit => 10, :offset => 50})
# generates
# SELECT * FROM suppliers LIMIT 10 OFFSET 50
def add_limit_offset!(sql, options)
if limit = options[:limit]
sql << " LIMIT #{sanitize_limit(limit)}"
@ -272,6 +271,10 @@ module ActiveRecord
execute "INSERT INTO #{quote_table_name(table_name)} (#{key_list.join(', ')}) VALUES (#{value_list.join(', ')})", 'Fixture Insert'
end
def null_insert_value
Arel.sql 'DEFAULT'
end
def empty_insert_statement_value
"VALUES(DEFAULT)"
end

View File

@ -279,12 +279,11 @@ module ActiveRecord
raise NotImplementedError, "change_column is not implemented"
end
# Sets a new default value for a column. If you want to set the default
# value to +NULL+, you are out of luck. You need to
# DatabaseStatements#execute the appropriate SQL statement yourself.
# Sets a new default value for a column.
# ===== Examples
# change_column_default(:suppliers, :qualification, 'new')
# change_column_default(:accounts, :authorized, 1)
# change_column_default(:users, :email, nil)
def change_column_default(table_name, column_name, default)
raise NotImplementedError, "change_column_default is not implemented"
end

View File

@ -453,7 +453,7 @@ module ActiveRecord
# If a pk is given, fallback to default sequence name.
# Don't fetch last insert id for a table without a pk.
if pk && sequence_name ||= default_sequence_name(table, pk)
last_insert_id(table, sequence_name)
last_insert_id(sequence_name)
end
end
end
@ -1038,8 +1038,9 @@ module ActiveRecord
end
# Returns the current ID of a table's sequence.
def last_insert_id(table, sequence_name) #:nodoc:
Integer(select_value("SELECT currval('#{sequence_name}')"))
def last_insert_id(sequence_name) #:nodoc:
r = exec_query("SELECT currval($1)", 'SQL', [[nil, sequence_name]])
Integer(r.rows.first.first)
end
# Executes a SELECT query and returns the results, performing any data type

View File

@ -336,6 +336,10 @@ module ActiveRecord
alter_table(table_name, :rename => {column_name.to_s => new_column_name.to_s})
end
def null_insert_value
Arel.sql 'NULL'
end
def empty_insert_statement_value
"VALUES(NULL)"
end

View File

@ -270,17 +270,9 @@ module ActiveRecord
# Creates a record with values matching those of the instance attributes
# and returns its id.
def create
if id.nil? && connection.prefetch_primary_key?(self.class.table_name)
self.id = connection.next_sequence_value(self.class.sequence_name)
end
attributes_values = arel_attributes_values(!id.nil?)
new_id = if attributes_values.empty?
self.class.unscoped.insert connection.empty_insert_statement_value
else
self.class.unscoped.insert attributes_values
end
new_id = self.class.unscoped.insert attributes_values
self.id ||= new_id

View File

@ -262,16 +262,30 @@ module ActiveRecord
end
def through_reflection
false
end
def through_reflection_foreign_key
nil
end
def source_reflection
nil
end
# A chain of reflections from this one back to the owner. For more see the explanation in
# ThroughReflection.
def chain
[self]
end
# An array of arrays of conditions. Each item in the outside array corresponds to a reflection
# in the #chain. The inside arrays are simply conditions (and each condition may itself be
# a hash, array, arel predicate, etc...)
def conditions
conditions = [options[:conditions]].compact
conditions << { type => active_record.base_class.name } if options[:as]
[conditions]
end
alias :source_macro :macro
def has_inverse?
@options[:inverse_of]
end
@ -363,7 +377,7 @@ module ActiveRecord
# Holds all the meta-data about a :through association as it was specified
# in the Active Record class.
class ThroughReflection < AssociationReflection #:nodoc:
delegate :association_primary_key, :foreign_type, :to => :source_reflection
delegate :foreign_key, :foreign_type, :association_foreign_key, :active_record_primary_key, :to => :source_reflection
# Gets the source of the through reflection. It checks both a singularized
# and pluralized form for <tt>:belongs_to</tt> or <tt>:has_many</tt>.
@ -392,6 +406,88 @@ module ActiveRecord
@through_reflection ||= active_record.reflect_on_association(options[:through])
end
# Returns an array of reflections which are involved in this association. Each item in the
# array corresponds to a table which will be part of the query for this association.
#
# The chain is built by recursively calling #chain on the source reflection and the through
# reflection. The base case for the recursion is a normal association, which just returns
# [self] as its #chain.
def chain
@chain ||= begin
chain = source_reflection.chain + through_reflection.chain
chain[0] = self # Use self so we don't lose the information from :source_type
chain
end
end
# Consider the following example:
#
# class Person
# has_many :articles
# has_many :comment_tags, :through => :articles
# end
#
# class Article
# has_many :comments
# has_many :comment_tags, :through => :comments, :source => :tags
# end
#
# class Comment
# has_many :tags
# end
#
# There may be conditions on Person.comment_tags, Article.comment_tags and/or Comment.tags,
# but only Comment.tags will be represented in the #chain. So this method creates an array
# of conditions corresponding to the chain. Each item in the #conditions array corresponds
# to an item in the #chain, and is itself an array of conditions from an arbitrary number
# of relevant reflections, plus any :source_type or polymorphic :as constraints.
def conditions
@conditions ||= begin
conditions = source_reflection.conditions
# Add to it the conditions from this reflection if necessary.
conditions.first << options[:conditions] if options[:conditions]
through_conditions = through_reflection.conditions
if options[:source_type]
through_conditions.first << { foreign_type => options[:source_type] }
end
# Recursively fill out the rest of the array from the through reflection
conditions += through_conditions
# And return
conditions
end
end
# The macro used by the source association
def source_macro
source_reflection.source_macro
end
# A through association is nested iff there would be more than one join table
def nested?
chain.length > 2 || through_reflection.macro == :has_and_belongs_to_many
end
# We want to use the klass from this reflection, rather than just delegate straight to
# the source_reflection, because the source_reflection may be polymorphic. We still
# need to respect the source_reflection's :primary_key option, though.
def association_primary_key
@association_primary_key ||= begin
# Get the "actual" source reflection if the immediate source reflection has a
# source reflection itself
source_reflection = self.source_reflection
while source_reflection.source_reflection
source_reflection = source_reflection.source_reflection
end
source_reflection.options[:primary_key] || klass.primary_key
end
end
# Gets an array of possible <tt>:through</tt> source reflection names:
#
# [:singularized, :pluralized]
@ -429,10 +525,6 @@ module ActiveRecord
raise HasManyThroughAssociationPolymorphicSourceError.new(active_record.name, self, source_reflection)
end
unless [:belongs_to, :has_many, :has_one].include?(source_reflection.macro) && source_reflection.options[:through].nil?
raise HasManyThroughSourceAssociationMacroError.new(self)
end
if macro == :has_one && through_reflection.collection?
raise HasOneThroughCantAssociateThroughCollection.new(active_record.name, self, through_reflection)
end
@ -440,14 +532,6 @@ module ActiveRecord
check_validity_of_inverse!
end
def through_reflection_primary_key
through_reflection.belongs_to? ? through_reflection.klass.primary_key : through_reflection.foreign_key
end
def through_reflection_foreign_key
through_reflection.foreign_key if through_reflection.belongs_to?
end
private
def derive_class_name
# get the class_name of the belongs_to association of the through reflection

View File

@ -30,15 +30,26 @@ module ActiveRecord
end
def insert(values)
im = arel.compile_insert values
im.into @table
primary_key_value = nil
if primary_key && Hash === values
primary_key_value = values[values.keys.find { |k|
k.name == primary_key
}]
if !primary_key_value && connection.prefetch_primary_key?(klass.table_name)
primary_key_value = connection.next_sequence_value(klass.sequence_name)
values[klass.arel_table[klass.primary_key]] = primary_key_value
end
end
im = arel.create_insert
im.into @table
if values.empty? # empty insert
im.values = im.create_values [connection.null_insert_value], []
else
im.insert values
end
@klass.connection.insert(

View File

@ -261,7 +261,7 @@ module ActiveRecord
)
join_nodes.each do |join|
join_dependency.table_aliases[join.left.name.downcase] = 1
join_dependency.alias_tracker.aliased_name_for(join.left.name.downcase)
end
join_dependency.graft(*stashed_association_joins)

View File

@ -15,17 +15,17 @@ class CascadedEagerLoadingTest < ActiveRecord::TestCase
def test_eager_association_loading_with_cascaded_two_levels
authors = Author.find(:all, :include=>{:posts=>:comments}, :order=>"authors.id")
assert_equal 2, authors.size
assert_equal 3, authors.size
assert_equal 5, authors[0].posts.size
assert_equal 1, authors[1].posts.size
assert_equal 3, authors[1].posts.size
assert_equal 10, authors[0].posts.collect{|post| post.comments.size }.inject(0){|sum,i| sum+i}
end
def test_eager_association_loading_with_cascaded_two_levels_and_one_level
authors = Author.find(:all, :include=>[{:posts=>:comments}, :categorizations], :order=>"authors.id")
assert_equal 2, authors.size
assert_equal 3, authors.size
assert_equal 5, authors[0].posts.size
assert_equal 1, authors[1].posts.size
assert_equal 3, authors[1].posts.size
assert_equal 10, authors[0].posts.collect{|post| post.comments.size }.inject(0){|sum,i| sum+i}
assert_equal 1, authors[0].categorizations.size
assert_equal 2, authors[1].categorizations.size
@ -51,24 +51,24 @@ class CascadedEagerLoadingTest < ActiveRecord::TestCase
categories = Category.joins(:categorizations).includes([{:posts=>:comments}, :authors])
assert_nothing_raised do
assert_equal 2, categories.count
assert_equal 2, categories.all.uniq.size # Must uniq since instantiating with inner joins will get dupes
assert_equal 3, categories.count
assert_equal 3, categories.all.uniq.size # Must uniq since instantiating with inner joins will get dupes
end
end
def test_cascaded_eager_association_loading_with_duplicated_includes
categories = Category.includes(:categorizations).includes(:categorizations => :author).where("categorizations.id is not null")
assert_nothing_raised do
assert_equal 2, categories.count
assert_equal 2, categories.all.size
assert_equal 3, categories.count
assert_equal 3, categories.all.size
end
end
def test_cascaded_eager_association_loading_with_twice_includes_edge_cases
categories = Category.includes(:categorizations => :author).includes(:categorizations => :post).where("posts.id is not null")
assert_nothing_raised do
assert_equal 2, categories.count
assert_equal 2, categories.all.size
assert_equal 3, categories.count
assert_equal 3, categories.all.size
end
end
@ -81,15 +81,15 @@ class CascadedEagerLoadingTest < ActiveRecord::TestCase
def test_eager_association_loading_with_cascaded_two_levels_with_two_has_many_associations
authors = Author.find(:all, :include=>{:posts=>[:comments, :categorizations]}, :order=>"authors.id")
assert_equal 2, authors.size
assert_equal 3, authors.size
assert_equal 5, authors[0].posts.size
assert_equal 1, authors[1].posts.size
assert_equal 3, authors[1].posts.size
assert_equal 10, authors[0].posts.collect{|post| post.comments.size }.inject(0){|sum,i| sum+i}
end
def test_eager_association_loading_with_cascaded_two_levels_and_self_table_reference
authors = Author.find(:all, :include=>{:posts=>[:comments, :author]}, :order=>"authors.id")
assert_equal 2, authors.size
assert_equal 3, authors.size
assert_equal 5, authors[0].posts.size
assert_equal authors(:david).name, authors[0].name
assert_equal [authors(:david).name], authors[0].posts.collect{|post| post.author.name}.uniq
@ -157,9 +157,9 @@ class CascadedEagerLoadingTest < ActiveRecord::TestCase
def test_eager_association_loading_where_first_level_returns_nil
authors = Author.find(:all, :include => {:post_about_thinking => :comments}, :order => 'authors.id DESC')
assert_equal [authors(:mary), authors(:david)], authors
assert_equal [authors(:bob), authors(:mary), authors(:david)], authors
assert_no_queries do
authors[1].post_about_thinking.comments.first
authors[2].post_about_thinking.comments.first
end
end
end

View File

@ -68,8 +68,8 @@ class EagerAssociationTest < ActiveRecord::TestCase
def test_with_ordering
list = Post.find(:all, :include => :comments, :order => "posts.id DESC")
[:eager_other, :sti_habtm, :sti_post_and_comments, :sti_comments,
:authorless, :thinking, :welcome
[:other_by_mary, :other_by_bob, :misc_by_mary, :misc_by_bob, :eager_other,
:sti_habtm, :sti_post_and_comments, :sti_comments, :authorless, :thinking, :welcome
].each_with_index do |post, index|
assert_equal posts(post), list[index]
end
@ -97,25 +97,25 @@ class EagerAssociationTest < ActiveRecord::TestCase
def test_preloading_has_many_in_multiple_queries_with_more_ids_than_database_can_handle
Post.connection.expects(:in_clause_length).at_least_once.returns(5)
posts = Post.find(:all, :include=>:comments)
assert_equal 7, posts.size
assert_equal 11, posts.size
end
def test_preloading_has_many_in_one_queries_when_database_has_no_limit_on_ids_it_can_handle
Post.connection.expects(:in_clause_length).at_least_once.returns(nil)
posts = Post.find(:all, :include=>:comments)
assert_equal 7, posts.size
assert_equal 11, posts.size
end
def test_preloading_habtm_in_multiple_queries_with_more_ids_than_database_can_handle
Post.connection.expects(:in_clause_length).at_least_once.returns(5)
posts = Post.find(:all, :include=>:categories)
assert_equal 7, posts.size
assert_equal 11, posts.size
end
def test_preloading_habtm_in_one_queries_when_database_has_no_limit_on_ids_it_can_handle
Post.connection.expects(:in_clause_length).at_least_once.returns(nil)
posts = Post.find(:all, :include=>:categories)
assert_equal 7, posts.size
assert_equal 11, posts.size
end
def test_load_associated_records_in_one_query_when_adapter_has_no_limit

View File

@ -642,12 +642,12 @@ class HasAndBelongsToManyAssociationsTest < ActiveRecord::TestCase
def test_find_grouped
all_posts_from_category1 = Post.find(:all, :conditions => "category_id = 1", :joins => :categories)
grouped_posts_of_category1 = Post.find(:all, :conditions => "category_id = 1", :group => "author_id", :select => 'count(posts.id) as posts_count', :joins => :categories)
assert_equal 4, all_posts_from_category1.size
assert_equal 1, grouped_posts_of_category1.size
assert_equal 5, all_posts_from_category1.size
assert_equal 2, grouped_posts_of_category1.size
end
def test_find_scoped_grouped
assert_equal 4, categories(:general).posts_grouped_by_title.size
assert_equal 5, categories(:general).posts_grouped_by_title.size
assert_equal 1, categories(:technology).posts_grouped_by_title.size
end

View File

@ -17,9 +17,10 @@ require 'models/developer'
require 'models/subscriber'
require 'models/book'
require 'models/subscription'
require 'models/categorization'
require 'models/category'
require 'models/essay'
require 'models/category'
require 'models/owner'
require 'models/categorization'
require 'models/member'
require 'models/membership'
require 'models/club'
@ -27,7 +28,7 @@ require 'models/club'
class HasManyThroughAssociationsTest < ActiveRecord::TestCase
fixtures :posts, :readers, :people, :comments, :authors, :categories, :taggings, :tags,
:owners, :pets, :toys, :jobs, :references, :companies, :members, :author_addresses,
:subscribers, :books, :subscriptions, :developers, :categorizations
:subscribers, :books, :subscriptions, :developers, :categorizations, :essays
# Dummies to force column loads so query counts are clean.
def setup
@ -656,6 +657,25 @@ class HasManyThroughAssociationsTest < ActiveRecord::TestCase
assert author.comments.include?(comment)
end
def test_has_many_through_polymorphic_with_primary_key_option
assert_equal [categories(:general)], authors(:david).essay_categories
authors = Author.joins(:essay_categories).where('categories.id' => categories(:general).id)
assert_equal authors(:david), authors.first
assert_equal [owners(:blackbeard)], authors(:david).essay_owners
authors = Author.joins(:essay_owners).where("owners.name = 'blackbeard'")
assert_equal authors(:david), authors.first
end
def test_has_many_through_with_primary_key_option
assert_equal [categories(:general)], authors(:david).essay_categories_2
authors = Author.joins(:essay_categories_2).where('categories.id' => categories(:general).id)
assert_equal authors(:david), authors.first
end
def test_size_of_through_association_should_increase_correctly_when_has_many_association_is_added
post = posts(:thinking)
readers = post.readers.size
@ -679,10 +699,10 @@ class HasManyThroughAssociationsTest < ActiveRecord::TestCase
end
def test_joining_has_many_through_belongs_to
posts = Post.joins(:author_categorizations).
posts = Post.joins(:author_categorizations).order('posts.id').
where('categorizations.id' => categorizations(:mary_thinking_sti).id)
assert_equal [posts(:eager_other)], posts
assert_equal [posts(:eager_other), posts(:misc_by_mary), posts(:other_by_mary)], posts
end
def test_select_chosen_fields_only

View File

@ -9,13 +9,16 @@ require 'models/member_detail'
require 'models/minivan'
require 'models/dashboard'
require 'models/speedometer'
require 'models/category'
require 'models/author'
require 'models/essay'
require 'models/owner'
require 'models/post'
require 'models/comment'
class HasOneThroughAssociationsTest < ActiveRecord::TestCase
fixtures :member_types, :members, :clubs, :memberships, :sponsors, :organizations, :minivans,
:dashboards, :speedometers, :authors, :posts, :comments
:dashboards, :speedometers, :authors, :posts, :comments, :categories, :essays, :owners
def setup
@member = members(:groucho)
@ -242,6 +245,25 @@ class HasOneThroughAssociationsTest < ActiveRecord::TestCase
end
end
def test_has_one_through_polymorphic_with_primary_key_option
assert_equal categories(:general), authors(:david).essay_category
authors = Author.joins(:essay_category).where('categories.id' => categories(:general).id)
assert_equal authors(:david), authors.first
assert_equal owners(:blackbeard), authors(:david).essay_owner
authors = Author.joins(:essay_owner).where("owners.name = 'blackbeard'")
assert_equal authors(:david), authors.first
end
def test_has_one_through_with_primary_key_option
assert_equal categories(:general), authors(:david).essay_category_2
authors = Author.joins(:essay_category_2).where('categories.id' => categories(:general).id)
assert_equal authors(:david), authors.first
end
def test_has_one_through_with_default_scope_on_join_model
assert_equal posts(:welcome).comments.order('id').first, authors(:david).comment_on_first_post
end

View File

@ -2,6 +2,7 @@ require "cases/helper"
require 'models/tag'
require 'models/tagging'
require 'models/post'
require 'models/rating'
require 'models/item'
require 'models/comment'
require 'models/author'
@ -288,7 +289,7 @@ class AssociationsJoinModelTest < ActiveRecord::TestCase
end
def test_has_many_going_through_join_model_with_custom_foreign_key
assert_equal [], posts(:thinking).authors
assert_equal [authors(:bob)], posts(:thinking).authors
assert_equal [authors(:mary)], posts(:authorless).authors
end
@ -305,7 +306,7 @@ class AssociationsJoinModelTest < ActiveRecord::TestCase
end
def test_has_many_through_with_custom_primary_key_on_has_many_source
assert_equal [authors(:david)], posts(:thinking).authors_using_custom_pk
assert_equal [authors(:david), authors(:bob)], posts(:thinking).authors_using_custom_pk.order('authors.id')
end
def test_both_scoped_and_explicit_joins_should_be_respected
@ -399,14 +400,6 @@ class AssociationsJoinModelTest < ActiveRecord::TestCase
end
end
def test_has_many_through_has_many_through
assert_raise(ActiveRecord::HasManyThroughSourceAssociationMacroError) { authors(:david).tags }
end
def test_has_many_through_habtm
assert_raise(ActiveRecord::HasManyThroughSourceAssociationMacroError) { authors(:david).post_categories }
end
def test_eager_load_has_many_through_has_many
author = Author.find :first, :conditions => ['name = ?', 'David'], :include => :comments, :order => 'comments.id'
SpecialComment.new; VerySpecialComment.new

View File

@ -0,0 +1,546 @@
require "cases/helper"
require 'models/author'
require 'models/post'
require 'models/person'
require 'models/reference'
require 'models/job'
require 'models/reader'
require 'models/comment'
require 'models/tag'
require 'models/tagging'
require 'models/subscriber'
require 'models/book'
require 'models/subscription'
require 'models/rating'
require 'models/member'
require 'models/member_detail'
require 'models/member_type'
require 'models/sponsor'
require 'models/club'
require 'models/organization'
require 'models/category'
require 'models/categorization'
require 'models/membership'
require 'models/essay'
class NestedThroughAssociationsTest < ActiveRecord::TestCase
fixtures :authors, :books, :posts, :subscriptions, :subscribers, :tags, :taggings,
:people, :readers, :references, :jobs, :ratings, :comments, :members, :member_details,
:member_types, :sponsors, :clubs, :organizations, :categories, :categories_posts,
:categorizations, :memberships, :essays
# Through associations can either use the has_many or has_one macros.
#
# has_many
# - Source reflection can be has_many, has_one, belongs_to or has_and_belongs_to_many
# - Through reflection can be has_many, has_one, belongs_to or has_and_belongs_to_many
#
# has_one
# - Source reflection can be has_one or belongs_to
# - Through reflection can be has_one or belongs_to
#
# Additionally, the source reflection and/or through reflection may be subject to
# polymorphism and/or STI.
#
# When testing these, we need to make sure it works via loading the association directly, or
# joining the association, or including the association. We also need to ensure that associations
# are readonly where relevant.
# has_many through
# Source: has_many through
# Through: has_many
def test_has_many_through_has_many_with_has_many_through_source_reflection
general = tags(:general)
assert_equal [general, general], authors(:david).tags
end
def test_has_many_through_has_many_with_has_many_through_source_reflection_preload
authors = assert_queries(5) { Author.includes(:tags).to_a }
general = tags(:general)
assert_no_queries do
assert_equal [general, general], authors.first.tags
end
end
def test_has_many_through_has_many_with_has_many_through_source_reflection_preload_via_joins
assert_includes_and_joins_equal(
Author.where('tags.id' => tags(:general).id),
[authors(:david)], :tags
)
# This ensures that the polymorphism of taggings is being observed correctly
authors = Author.joins(:tags).where('taggings.taggable_type' => 'FakeModel')
assert authors.empty?
end
# has_many through
# Source: has_many
# Through: has_many through
def test_has_many_through_has_many_through_with_has_many_source_reflection
luke, david = subscribers(:first), subscribers(:second)
assert_equal [luke, david, david], authors(:david).subscribers.order('subscribers.nick')
end
def test_has_many_through_has_many_through_with_has_many_source_reflection_preload
luke, david = subscribers(:first), subscribers(:second)
authors = assert_queries(4) { Author.includes(:subscribers).to_a }
assert_no_queries do
assert_equal [luke, david, david], authors.first.subscribers.sort_by(&:nick)
end
end
def test_has_many_through_has_many_through_with_has_many_source_reflection_preload_via_joins
# All authors with subscribers where one of the subscribers' nick is 'alterself'
assert_includes_and_joins_equal(
Author.where('subscribers.nick' => 'alterself'),
[authors(:david)], :subscribers
)
end
# has_many through
# Source: has_one through
# Through: has_one
def test_has_many_through_has_one_with_has_one_through_source_reflection
assert_equal [member_types(:founding)], members(:groucho).nested_member_types
end
def test_has_many_through_has_one_with_has_one_through_source_reflection_preload
members = assert_queries(4) { Member.includes(:nested_member_types).to_a }
founding = member_types(:founding)
assert_no_queries do
assert_equal [founding], members.first.nested_member_types
end
end
def test_has_many_through_has_one_with_has_one_through_source_reflection_preload_via_joins
assert_includes_and_joins_equal(
Member.where('member_types.id' => member_types(:founding).id),
[members(:groucho)], :nested_member_types
)
end
# has_many through
# Source: has_one
# Through: has_one through
def test_has_many_through_has_one_through_with_has_one_source_reflection
assert_equal [sponsors(:moustache_club_sponsor_for_groucho)], members(:groucho).nested_sponsors
end
def test_has_many_through_has_one_through_with_has_one_source_reflection_preload
members = assert_queries(4) { Member.includes(:nested_sponsors).to_a }
mustache = sponsors(:moustache_club_sponsor_for_groucho)
assert_no_queries do
assert_equal [mustache], members.first.nested_sponsors
end
end
def test_has_many_through_has_one_through_with_has_one_source_reflection_preload_via_joins
assert_includes_and_joins_equal(
Member.where('sponsors.id' => sponsors(:moustache_club_sponsor_for_groucho).id),
[members(:groucho)], :nested_sponsors
)
end
# has_many through
# Source: has_many through
# Through: has_one
def test_has_many_through_has_one_with_has_many_through_source_reflection
groucho_details, other_details = member_details(:groucho), member_details(:some_other_guy)
assert_equal [groucho_details, other_details],
members(:groucho).organization_member_details.order('member_details.id')
end
def test_has_many_through_has_one_with_has_many_through_source_reflection_preload
members = assert_queries(4) { Member.includes(:organization_member_details).to_a.sort_by(&:id) }
groucho_details, other_details = member_details(:groucho), member_details(:some_other_guy)
assert_no_queries do
assert_equal [groucho_details, other_details], members.first.organization_member_details.sort_by(&:id)
end
end
def test_has_many_through_has_one_with_has_many_through_source_reflection_preload_via_joins
assert_includes_and_joins_equal(
Member.where('member_details.id' => member_details(:groucho).id).order('member_details.id'),
[members(:groucho), members(:some_other_guy)], :organization_member_details
)
members = Member.joins(:organization_member_details).
where('member_details.id' => 9)
assert members.empty?
end
# has_many through
# Source: has_many
# Through: has_one through
def test_has_many_through_has_one_through_with_has_many_source_reflection
groucho_details, other_details = member_details(:groucho), member_details(:some_other_guy)
assert_equal [groucho_details, other_details],
members(:groucho).organization_member_details_2.order('member_details.id')
end
def test_has_many_through_has_one_through_with_has_many_source_reflection_preload
members = assert_queries(4) { Member.includes(:organization_member_details_2).to_a.sort_by(&:id) }
groucho_details, other_details = member_details(:groucho), member_details(:some_other_guy)
assert_no_queries do
assert_equal [groucho_details, other_details], members.first.organization_member_details_2.sort_by(&:id)
end
end
def test_has_many_through_has_one_through_with_has_many_source_reflection_preload_via_joins
assert_includes_and_joins_equal(
Member.where('member_details.id' => member_details(:groucho).id).order('member_details.id'),
[members(:groucho), members(:some_other_guy)], :organization_member_details_2
)
members = Member.joins(:organization_member_details_2).
where('member_details.id' => 9)
assert members.empty?
end
# has_many through
# Source: has_and_belongs_to_many
# Through: has_many
def test_has_many_through_has_many_with_has_and_belongs_to_many_source_reflection
general, cooking = categories(:general), categories(:cooking)
assert_equal [general, cooking], authors(:bob).post_categories.order('categories.id')
end
def test_has_many_through_has_many_with_has_and_belongs_to_many_source_reflection_preload
authors = assert_queries(3) { Author.includes(:post_categories).to_a.sort_by(&:id) }
general, cooking = categories(:general), categories(:cooking)
assert_no_queries do
assert_equal [general, cooking], authors[2].post_categories.sort_by(&:id)
end
end
def test_has_many_through_has_many_with_has_and_belongs_to_many_source_reflection_preload_via_joins
assert_includes_and_joins_equal(
Author.where('categories.id' => categories(:cooking).id),
[authors(:bob)], :post_categories
)
end
# has_many through
# Source: has_many
# Through: has_and_belongs_to_many
def test_has_many_through_has_and_belongs_to_many_with_has_many_source_reflection
greetings, more = comments(:greetings), comments(:more_greetings)
assert_equal [greetings, more], categories(:technology).post_comments.order('comments.id')
end
def test_has_many_through_has_and_belongs_to_many_with_has_many_source_reflection_preload
categories = assert_queries(3) { Category.includes(:post_comments).to_a.sort_by(&:id) }
greetings, more = comments(:greetings), comments(:more_greetings)
assert_no_queries do
assert_equal [greetings, more], categories[1].post_comments.sort_by(&:id)
end
end
def test_has_many_through_has_and_belongs_to_many_with_has_many_source_reflection_preload_via_joins
assert_includes_and_joins_equal(
Category.where('comments.id' => comments(:more_greetings).id).order('comments.id'),
[categories(:general), categories(:technology)], :post_comments
)
end
# has_many through
# Source: has_many through a habtm
# Through: has_many through
def test_has_many_through_has_many_with_has_many_through_habtm_source_reflection
greetings, more = comments(:greetings), comments(:more_greetings)
assert_equal [greetings, more], authors(:bob).category_post_comments.order('comments.id')
end
def test_has_many_through_has_many_with_has_many_through_habtm_source_reflection_preload
authors = assert_queries(5) { Author.includes(:category_post_comments).to_a.sort_by(&:id) }
greetings, more = comments(:greetings), comments(:more_greetings)
assert_no_queries do
assert_equal [greetings, more], authors[2].category_post_comments.sort_by(&:id)
end
end
def test_has_many_through_has_many_with_has_many_through_habtm_source_reflection_preload_via_joins
assert_includes_and_joins_equal(
Author.where('comments.id' => comments(:does_it_hurt).id).order('authors.id'),
[authors(:david), authors(:mary)], :category_post_comments
)
end
# has_many through
# Source: belongs_to
# Through: has_many through
def test_has_many_through_has_many_through_with_belongs_to_source_reflection
assert_equal [tags(:general), tags(:general)], authors(:david).tagging_tags
end
def test_has_many_through_has_many_through_with_belongs_to_source_reflection_preload
authors = assert_queries(5) { Author.includes(:tagging_tags).to_a }
general = tags(:general)
assert_no_queries do
assert_equal [general, general], authors.first.tagging_tags
end
end
def test_has_many_through_has_many_through_with_belongs_to_source_reflection_preload_via_joins
assert_includes_and_joins_equal(
Author.where('tags.id' => tags(:general).id),
[authors(:david)], :tagging_tags
)
end
# has_many through
# Source: has_many through
# Through: belongs_to
def test_has_many_through_belongs_to_with_has_many_through_source_reflection
welcome_general, thinking_general = taggings(:welcome_general), taggings(:thinking_general)
assert_equal [welcome_general, thinking_general],
categorizations(:david_welcome_general).post_taggings.order('taggings.id')
end
def test_has_many_through_belongs_to_with_has_many_through_source_reflection_preload
categorizations = assert_queries(4) { Categorization.includes(:post_taggings).to_a.sort_by(&:id) }
welcome_general, thinking_general = taggings(:welcome_general), taggings(:thinking_general)
assert_no_queries do
assert_equal [welcome_general, thinking_general], categorizations.first.post_taggings.sort_by(&:id)
end
end
def test_has_many_through_belongs_to_with_has_many_through_source_reflection_preload_via_joins
assert_includes_and_joins_equal(
Categorization.where('taggings.id' => taggings(:welcome_general).id).order('taggings.id'),
[categorizations(:david_welcome_general)], :post_taggings
)
end
# has_one through
# Source: has_one through
# Through: has_one
def test_has_one_through_has_one_with_has_one_through_source_reflection
assert_equal member_types(:founding), members(:groucho).nested_member_type
end
def test_has_one_through_has_one_with_has_one_through_source_reflection_preload
members = assert_queries(4) { Member.includes(:nested_member_type).to_a.sort_by(&:id) }
founding = member_types(:founding)
assert_no_queries do
assert_equal founding, members.first.nested_member_type
end
end
def test_has_one_through_has_one_with_has_one_through_source_reflection_preload_via_joins
assert_includes_and_joins_equal(
Member.where('member_types.id' => member_types(:founding).id),
[members(:groucho)], :nested_member_type
)
end
# has_one through
# Source: belongs_to
# Through: has_one through
def test_has_one_through_has_one_through_with_belongs_to_source_reflection
assert_equal categories(:general), members(:groucho).club_category
end
def test_has_one_through_has_one_through_with_belongs_to_source_reflection_preload
members = assert_queries(4) { Member.includes(:club_category).to_a.sort_by(&:id) }
general = categories(:general)
assert_no_queries do
assert_equal general, members.first.club_category
end
end
def test_has_one_through_has_one_through_with_belongs_to_source_reflection_preload_via_joins
assert_includes_and_joins_equal(
Member.where('categories.id' => categories(:technology).id),
[members(:blarpy_winkup)], :club_category
)
end
def test_distinct_has_many_through_a_has_many_through_association_on_source_reflection
author = authors(:david)
assert_equal [tags(:general)], author.distinct_tags
end
def test_distinct_has_many_through_a_has_many_through_association_on_through_reflection
author = authors(:david)
assert_equal [subscribers(:first), subscribers(:second)],
author.distinct_subscribers.order('subscribers.nick')
end
def test_nested_has_many_through_with_a_table_referenced_multiple_times
author = authors(:bob)
assert_equal [posts(:misc_by_bob), posts(:misc_by_mary), posts(:other_by_bob), posts(:other_by_mary)],
author.similar_posts.sort_by(&:id)
# Mary and Bob both have posts in misc, but they are the only ones.
authors = Author.joins(:similar_posts).where('posts.id' => posts(:misc_by_bob).id)
assert_equal [authors(:mary), authors(:bob)], authors.uniq.sort_by(&:id)
# Check the polymorphism of taggings is being observed correctly (in both joins)
authors = Author.joins(:similar_posts).where('taggings.taggable_type' => 'FakeModel')
assert authors.empty?
authors = Author.joins(:similar_posts).where('taggings_authors_join.taggable_type' => 'FakeModel')
assert authors.empty?
end
def test_has_many_through_with_foreign_key_option_on_through_reflection
assert_equal [posts(:welcome), posts(:authorless)], people(:david).agents_posts.order('posts.id')
assert_equal [authors(:david)], references(:david_unicyclist).agents_posts_authors
references = Reference.joins(:agents_posts_authors).where('authors.id' => authors(:david).id)
assert_equal [references(:david_unicyclist)], references
end
def test_has_many_through_with_foreign_key_option_on_source_reflection
assert_equal [people(:michael), people(:susan)], jobs(:unicyclist).agents.order('people.id')
jobs = Job.joins(:agents)
assert_equal [jobs(:unicyclist), jobs(:unicyclist)], jobs
end
def test_has_many_through_with_sti_on_through_reflection
ratings = posts(:sti_comments).special_comments_ratings.sort_by(&:id)
assert_equal [ratings(:special_comment_rating), ratings(:sub_special_comment_rating)], ratings
# Ensure STI is respected in the join
scope = Post.joins(:special_comments_ratings).where(:id => posts(:sti_comments).id)
assert scope.where("comments.type" => "Comment").empty?
assert !scope.where("comments.type" => "SpecialComment").empty?
assert !scope.where("comments.type" => "SubSpecialComment").empty?
end
def test_has_many_through_with_sti_on_nested_through_reflection
taggings = posts(:sti_comments).special_comments_ratings_taggings
assert_equal [taggings(:special_comment_rating)], taggings
scope = Post.joins(:special_comments_ratings_taggings).where(:id => posts(:sti_comments).id)
assert scope.where("comments.type" => "Comment").empty?
assert !scope.where("comments.type" => "SpecialComment").empty?
end
def test_nested_has_many_through_writers_should_raise_error
david = authors(:david)
subscriber = subscribers(:first)
assert_raises(ActiveRecord::HasManyThroughNestedAssociationsAreReadonly) do
david.subscribers = [subscriber]
end
assert_raises(ActiveRecord::HasManyThroughNestedAssociationsAreReadonly) do
david.subscriber_ids = [subscriber.id]
end
assert_raises(ActiveRecord::HasManyThroughNestedAssociationsAreReadonly) do
david.subscribers << subscriber
end
assert_raises(ActiveRecord::HasManyThroughNestedAssociationsAreReadonly) do
david.subscribers.delete(subscriber)
end
assert_raises(ActiveRecord::HasManyThroughNestedAssociationsAreReadonly) do
david.subscribers.clear
end
assert_raises(ActiveRecord::HasManyThroughNestedAssociationsAreReadonly) do
david.subscribers.build
end
assert_raises(ActiveRecord::HasManyThroughNestedAssociationsAreReadonly) do
david.subscribers.create
end
end
def test_nested_has_one_through_writers_should_raise_error
groucho = members(:groucho)
founding = member_types(:founding)
assert_raises(ActiveRecord::HasManyThroughNestedAssociationsAreReadonly) do
groucho.nested_member_type = founding
end
end
def test_nested_has_many_through_with_conditions_on_through_associations
assert_equal [tags(:blue)], authors(:bob).misc_post_first_blue_tags
end
def test_nested_has_many_through_with_conditions_on_through_associations_preload
assert Author.where('tags.id' => 100).joins(:misc_post_first_blue_tags).empty?
authors = assert_queries(3) { Author.includes(:misc_post_first_blue_tags).to_a.sort_by(&:id) }
blue = tags(:blue)
assert_no_queries do
assert_equal [blue], authors[2].misc_post_first_blue_tags
end
end
def test_nested_has_many_through_with_conditions_on_through_associations_preload_via_joins
# Pointless condition to force single-query loading
assert_includes_and_joins_equal(
Author.where('tags.id = tags.id'),
[authors(:bob)], :misc_post_first_blue_tags
)
end
def test_nested_has_many_through_with_conditions_on_source_associations
assert_equal [tags(:blue)], authors(:bob).misc_post_first_blue_tags_2
end
def test_nested_has_many_through_with_conditions_on_source_associations_preload
authors = assert_queries(4) { Author.includes(:misc_post_first_blue_tags_2).to_a.sort_by(&:id) }
blue = tags(:blue)
assert_no_queries do
assert_equal [blue], authors[2].misc_post_first_blue_tags_2
end
end
def test_nested_has_many_through_with_conditions_on_source_associations_preload_via_joins
# Pointless condition to force single-query loading
assert_includes_and_joins_equal(
Author.where('tags.id = tags.id'),
[authors(:bob)], :misc_post_first_blue_tags_2
)
end
def test_nested_has_many_through_with_foreign_key_option_on_the_source_reflection_through_reflection
assert_equal [categories(:general)], organizations(:nsa).author_essay_categories
organizations = Organization.joins(:author_essay_categories).
where('categories.id' => categories(:general).id)
assert_equal [organizations(:nsa)], organizations
assert_equal categories(:general), organizations(:nsa).author_owned_essay_category
organizations = Organization.joins(:author_owned_essay_category).
where('categories.id' => categories(:general).id)
assert_equal [organizations(:nsa)], organizations
end
private
def assert_includes_and_joins_equal(query, expected, association)
actual = assert_queries(1) { query.joins(association).to_a.uniq }
assert_equal expected, actual
actual = assert_queries(1) { query.includes(association).to_a.uniq }
assert_equal expected, actual
end
end

View File

@ -118,22 +118,18 @@ class AttributeMethodsTest < ActiveRecord::TestCase
end
def test_read_attributes_before_type_cast_on_datetime
developer = Developer.find(:first)
if current_adapter?(:Mysql2Adapter, :OracleAdapter)
# Mysql2 and Oracle adapters keep the value in Time instance
assert_equal developer.created_at.to_s(:db), developer.attributes_before_type_cast["created_at"].to_s(:db)
else
assert_equal developer.created_at.to_s(:db), developer.attributes_before_type_cast["created_at"].to_s
in_time_zone "Pacific Time (US & Canada)" do
record = @target.new
record.written_on = "345643456"
assert_equal "345643456", record.written_on_before_type_cast
assert_equal nil, record.written_on
record.written_on = "2009-10-11 12:13:14"
assert_equal "2009-10-11 12:13:14", record.written_on_before_type_cast
assert_equal Time.zone.parse("2009-10-11 12:13:14"), record.written_on
assert_equal ActiveSupport::TimeZone["Pacific Time (US & Canada)"], record.written_on.time_zone
end
developer.created_at = "345643456"
assert_equal developer.created_at_before_type_cast, "345643456"
assert_equal developer.created_at, nil
developer.created_at = "2010-03-21 21:23:32"
assert_equal developer.created_at_before_type_cast, "2010-03-21 21:23:32"
assert_equal developer.created_at, Time.parse("2010-03-21 21:23:32")
end
def test_hash_content

View File

@ -45,6 +45,8 @@ class ReadonlyTitlePost < Post
attr_readonly :title
end
class Weird < ActiveRecord::Base; end
class Boolean < ActiveRecord::Base; end
class BasicsTest < ActiveRecord::TestCase
@ -477,6 +479,16 @@ class BasicsTest < ActiveRecord::TestCase
assert_equal "changed", post.body
end
def test_non_valid_identifier_column_name
weird = Weird.create('a$b' => 'value')
weird.reload
assert_equal 'value', weird.send('a$b')
weird.update_attribute('a$b', 'value2')
weird.reload
assert_equal 'value2', weird.send('a$b')
end
def test_multiparameter_attributes_on_date
attributes = { "last_read(1i)" => "2004", "last_read(2i)" => "6", "last_read(3i)" => "24" }
topic = Topic.find(1)

View File

@ -25,7 +25,7 @@ class EachTest < ActiveRecord::TestCase
end
def test_each_should_execute_if_id_is_in_select
assert_queries(4) do
assert_queries(6) do
Post.find_each(:select => "id, title, type", :batch_size => 2) do |post|
assert_kind_of Post, post
end

View File

@ -124,11 +124,13 @@ class FinderTest < ActiveRecord::TestCase
def test_find_all_with_limit_and_offset_and_multiple_order_clauses
first_three_posts = Post.find :all, :order => 'author_id, id', :limit => 3, :offset => 0
second_three_posts = Post.find :all, :order => ' author_id,id ', :limit => 3, :offset => 3
last_posts = Post.find :all, :order => ' author_id, id ', :limit => 3, :offset => 6
third_three_posts = Post.find :all, :order => ' author_id, id ', :limit => 3, :offset => 6
last_posts = Post.find :all, :order => ' author_id, id ', :limit => 3, :offset => 9
assert_equal [[0,3],[1,1],[1,2]], first_three_posts.map { |p| [p.author_id, p.id] }
assert_equal [[1,4],[1,5],[1,6]], second_three_posts.map { |p| [p.author_id, p.id] }
assert_equal [[2,7]], last_posts.map { |p| [p.author_id, p.id] }
assert_equal [[2,7],[2,9],[2,11]], third_three_posts.map { |p| [p.author_id, p.id] }
assert_equal [[3,8],[3,10]], last_posts.map { |p| [p.author_id, p.id] }
end

View File

@ -13,5 +13,39 @@ class HabtmDestroyOrderTest < ActiveRecord::TestCase
sicp.destroy
end
end
assert !sicp.destroyed?
end
test "not destroying a student with lessons leaves student<=>lesson association intact" do
# test a normal before_destroy doesn't destroy the habtm joins
begin
sicp = Lesson.new(:name => "SICP")
ben = Student.new(:name => "Ben Bitdiddle")
# add a before destroy to student
Student.class_eval do
before_destroy do
raise ActiveRecord::Rollback unless lessons.empty?
end
end
ben.lessons << sicp
ben.save!
ben.destroy
assert !ben.reload.lessons.empty?
ensure
# get rid of it so Student is still like it was
Student.reset_callbacks(:destroy)
end
end
test "not destroying a lesson with students leaves student<=>lesson association intact" do
# test a more aggressive before_destroy doesn't destroy the habtm joins and still throws the exception
sicp = Lesson.new(:name => "SICP")
ben = Student.new(:name => "Ben Bitdiddle")
sicp.students << ben
sicp.save!
assert_raises LessonError do
sicp.destroy
end
assert !sicp.reload.students.empty?
end
end

View File

@ -207,31 +207,31 @@ class IdentityMapTest < ActiveRecord::TestCase
def test_find_with_preloaded_associations
assert_queries(2) do
posts = Post.preload(:comments)
posts = Post.preload(:comments).order('posts.id')
assert posts.first.comments.first
end
# With IM we'll retrieve post object from previous query, it'll have comments
# already preloaded from first call
assert_queries(1) do
posts = Post.preload(:comments).to_a
posts = Post.preload(:comments).order('posts.id')
assert posts.first.comments.first
end
assert_queries(2) do
posts = Post.preload(:author)
posts = Post.preload(:author).order('posts.id')
assert posts.first.author
end
# With IM we'll retrieve post object from previous query, it'll have comments
# already preloaded from first call
assert_queries(1) do
posts = Post.preload(:author).to_a
posts = Post.preload(:author).order('posts.id')
assert posts.first.author
end
assert_queries(1) do
posts = Post.preload(:author, :comments).to_a
posts = Post.preload(:author, :comments).order('posts.id')
assert posts.first.author
assert posts.first.comments.first
end
@ -239,22 +239,22 @@ class IdentityMapTest < ActiveRecord::TestCase
def test_find_with_included_associations
assert_queries(2) do
posts = Post.includes(:comments)
posts = Post.includes(:comments).order('posts.id')
assert posts.first.comments.first
end
assert_queries(1) do
posts = Post.scoped.includes(:comments)
posts = Post.scoped.includes(:comments).order('posts.id')
assert posts.first.comments.first
end
assert_queries(2) do
posts = Post.includes(:author)
posts = Post.includes(:author).order('posts.id')
assert posts.first.author
end
assert_queries(1) do
posts = Post.includes(:author, :comments).to_a
posts = Post.includes(:author, :comments).order('posts.id')
assert posts.first.author
assert posts.first.comments.first
end

View File

@ -181,7 +181,11 @@ class DatabaseConnectedJsonEncodingTest < ActiveRecord::TestCase
def test_should_allow_except_option_for_list_of_authors
ActiveRecord::Base.include_root_in_json = false
authors = [@david, @mary]
assert_equal %([{"id":1},{"id":2}]), ActiveSupport::JSON.encode(authors, :except => [:name, :author_address_id, :author_address_extra_id])
encoded = ActiveSupport::JSON.encode(authors, :except => [
:name, :author_address_id, :author_address_extra_id,
:organization_id, :owned_essay_id
])
assert_equal %([{"id":1},{"id":2}]), encoded
ensure
ActiveRecord::Base.include_root_in_json = true
end
@ -196,7 +200,7 @@ class DatabaseConnectedJsonEncodingTest < ActiveRecord::TestCase
)
['"name":"David"', '"posts":[', '{"id":1}', '{"id":2}', '{"id":4}',
'{"id":5}', '{"id":6}', '"name":"Mary"', '"posts":[{"id":7}]'].each do |fragment|
'{"id":5}', '{"id":6}', '"name":"Mary"', '"posts":[', '{"id":7}', '{"id":9}'].each do |fragment|
assert json.include?(fragment), json
end
end

View File

@ -7,9 +7,16 @@ require 'models/subscriber'
require 'models/ship'
require 'models/pirate'
require 'models/price_estimate'
require 'models/tagging'
require 'models/essay'
require 'models/author'
require 'models/organization'
require 'models/post'
require 'models/tagging'
require 'models/category'
require 'models/book'
require 'models/subscriber'
require 'models/subscription'
require 'models/tag'
require 'models/sponsor'
class ReflectionTest < ActiveRecord::TestCase
@ -195,10 +202,54 @@ class ReflectionTest < ActiveRecord::TestCase
assert_kind_of ThroughReflection, Subscriber.reflect_on_association(:books)
end
def test_chain
expected = [
Organization.reflect_on_association(:author_essay_categories),
Author.reflect_on_association(:essays),
Organization.reflect_on_association(:authors)
]
actual = Organization.reflect_on_association(:author_essay_categories).chain
assert_equal expected, actual
end
def test_conditions
expected = [
[{ :tags => { :name => 'Blue' } }],
[{ :taggings => { :comment => 'first' } }, { "taggable_type" => "Post" }],
[{ :posts => { :title => ['misc post by bob', 'misc post by mary'] } }]
]
actual = Author.reflect_on_association(:misc_post_first_blue_tags).conditions
assert_equal expected, actual
expected = [
[{ :tags => { :name => 'Blue' } }, { :taggings => { :comment => 'first' } }, { :posts => { :title => ['misc post by bob', 'misc post by mary'] } }],
[{ "taggable_type" => "Post" }],
[]
]
actual = Author.reflect_on_association(:misc_post_first_blue_tags_2).conditions
assert_equal expected, actual
end
def test_nested?
assert !Author.reflect_on_association(:comments).nested?
assert Author.reflect_on_association(:tags).nested?
# Only goes :through once, but the through_reflection is a has_and_belongs_to_many, so this is
# a nested through association
assert Category.reflect_on_association(:post_comments).nested?
end
def test_association_primary_key
assert_equal "id", Author.reflect_on_association(:posts).association_primary_key.to_s
# Normal association
assert_equal "id", Author.reflect_on_association(:posts).association_primary_key.to_s
assert_equal "name", Author.reflect_on_association(:essay).association_primary_key.to_s
assert_equal "id", Tagging.reflect_on_association(:taggable).association_primary_key.to_s
assert_equal "id", Tagging.reflect_on_association(:taggable).association_primary_key.to_s
# Through association (uses the :primary_key option from the source reflection)
assert_equal "nick", Author.reflect_on_association(:subscribers).association_primary_key.to_s
assert_equal "name", Author.reflect_on_association(:essay_category).association_primary_key.to_s
assert_equal "custom_primary_key", Author.reflect_on_association(:tags_with_primary_key).association_primary_key.to_s # nested
end
def test_active_record_primary_key

View File

@ -335,7 +335,7 @@ class DefaultScopingTest < ActiveRecord::TestCase
end
records = klass.all
assert_equal 1, records.length
assert_equal 3, records.length
assert_equal 2, records.first.author_id
end

View File

@ -165,7 +165,7 @@ class RelationTest < ActiveRecord::TestCase
def test_finding_with_complex_order
tags = Tag.includes(:taggings).order("REPLACE('abc', taggings.taggable_type, taggings.taggable_type)").to_a
assert_equal 2, tags.length
assert_equal 3, tags.length
end
def test_finding_with_order_limit_and_offset
@ -281,27 +281,27 @@ class RelationTest < ActiveRecord::TestCase
def test_find_with_preloaded_associations
assert_queries(2) do
posts = Post.preload(:comments)
posts = Post.preload(:comments).order('posts.id')
assert posts.first.comments.first
end
assert_queries(ActiveRecord::IdentityMap.enabled? ? 1 : 2) do
posts = Post.preload(:comments).to_a
posts = Post.preload(:comments).order('posts.id')
assert posts.first.comments.first
end
assert_queries(2) do
posts = Post.preload(:author)
posts = Post.preload(:author).order('posts.id')
assert posts.first.author
end
assert_queries(ActiveRecord::IdentityMap.enabled? ? 1 : 2) do
posts = Post.preload(:author).to_a
posts = Post.preload(:author).order('posts.id')
assert posts.first.author
end
assert_queries(ActiveRecord::IdentityMap.enabled? ? 1 : 3) do
posts = Post.preload(:author, :comments).to_a
posts = Post.preload(:author, :comments).order('posts.id')
assert posts.first.author
assert posts.first.comments.first
end
@ -309,22 +309,22 @@ class RelationTest < ActiveRecord::TestCase
def test_find_with_included_associations
assert_queries(2) do
posts = Post.includes(:comments)
posts = Post.includes(:comments).order('posts.id')
assert posts.first.comments.first
end
assert_queries(ActiveRecord::IdentityMap.enabled? ? 1 : 2) do
posts = Post.scoped.includes(:comments)
posts = Post.scoped.includes(:comments).order('posts.id')
assert posts.first.comments.first
end
assert_queries(2) do
posts = Post.includes(:author)
posts = Post.includes(:author).order('posts.id')
assert posts.first.author
end
assert_queries(ActiveRecord::IdentityMap.enabled? ? 1 : 3) do
posts = Post.includes(:author, :comments).to_a
posts = Post.includes(:author, :comments).order('posts.id')
assert posts.first.author
assert posts.first.comments.first
end
@ -538,7 +538,7 @@ class RelationTest < ActiveRecord::TestCase
def test_last
authors = Author.scoped
assert_equal authors(:mary), authors.last
assert_equal authors(:bob), authors.last
end
def test_destroy_all
@ -618,22 +618,22 @@ class RelationTest < ActiveRecord::TestCase
def test_count
posts = Post.scoped
assert_equal 7, posts.count
assert_equal 7, posts.count(:all)
assert_equal 7, posts.count(:id)
assert_equal 11, posts.count
assert_equal 11, posts.count(:all)
assert_equal 11, posts.count(:id)
assert_equal 1, posts.where('comments_count > 1').count
assert_equal 5, posts.where(:comments_count => 0).count
assert_equal 9, posts.where(:comments_count => 0).count
end
def test_count_with_distinct
posts = Post.scoped
assert_equal 3, posts.count(:comments_count, :distinct => true)
assert_equal 7, posts.count(:comments_count, :distinct => false)
assert_equal 11, posts.count(:comments_count, :distinct => false)
assert_equal 3, posts.select(:comments_count).count(:distinct => true)
assert_equal 7, posts.select(:comments_count).count(:distinct => false)
assert_equal 11, posts.select(:comments_count).count(:distinct => false)
end
def test_count_explicit_columns
@ -643,7 +643,7 @@ class RelationTest < ActiveRecord::TestCase
assert_equal [0], posts.select('comments_count').where('id is not null').group('id').order('id').count.values.uniq
assert_equal 0, posts.where('id is not null').select('comments_count').count
assert_equal 7, posts.select('comments_count').count('id')
assert_equal 11, posts.select('comments_count').count('id')
assert_equal 0, posts.select('comments_count').count
assert_equal 0, posts.count(:comments_count)
assert_equal 0, posts.count('comments_count')
@ -658,12 +658,12 @@ class RelationTest < ActiveRecord::TestCase
def test_size
posts = Post.scoped
assert_queries(1) { assert_equal 7, posts.size }
assert_queries(1) { assert_equal 11, posts.size }
assert ! posts.loaded?
best_posts = posts.where(:comments_count => 0)
best_posts.to_a # force load
assert_no_queries { assert_equal 5, best_posts.size }
assert_no_queries { assert_equal 9, best_posts.size }
end
def test_count_complex_chained_relations

View File

@ -3,7 +3,13 @@ david:
name: David
author_address_id: 1
author_address_extra_id: 2
organization_id: No Such Agency
owned_essay_id: A Modest Proposal
mary:
id: 2
name: Mary
bob:
id: 3
name: Bob

View File

@ -1,7 +1,9 @@
awdr:
author_id: 1
id: 1
name: "Agile Web Development with Rails"
rfr:
author_id: 1
id: 2
name: "Ruby for Rails"

View File

@ -12,3 +12,8 @@ sti_test:
id: 3
name: Special category
type: SpecialCategory
cooking:
id: 4
name: Cooking
type: Category

View File

@ -21,3 +21,11 @@ sti_test_sti_habtm:
general_hello:
category_id: 1
post_id: 4
general_misc_by_bob:
category_id: 1
post_id: 8
cooking_misc_by_bob:
category_id: 4
post_id: 8

View File

@ -15,3 +15,9 @@ mary_thinking_general:
author_id: 2
post_id: 2
category_id: 1
bob_misc_by_bob_technology:
id: 4
author_id: 3
post_id: 8
category_id: 2

View File

@ -1,6 +1,8 @@
boring_club:
name: Banana appreciation society
category_id: 1
moustache_club:
name: Moustache and Eyebrow Fancier Club
crazy_club:
name: Skull and bones
name: Skull and bones
category_id: 2

6
activerecord/test/fixtures/essays.yml vendored Normal file
View File

@ -0,0 +1,6 @@
david_modest_proposal:
name: A Modest Proposal
writer_type: Author
writer_id: David
category_id: General
author_id: David

View File

@ -0,0 +1,8 @@
groucho:
id: 1
member_id: 1
organization: nsa
some_other_guy:
id: 2
member_id: 2
organization: nsa

View File

@ -6,3 +6,6 @@ some_other_guy:
id: 2
name: Englebert Humperdink
member_type_id: 2
blarpy_winkup:
id: 3
name: Blarpy Winkup

View File

@ -18,3 +18,10 @@ other_guys_membership:
member_id: 2
favourite: false
type: CurrentMembership
blarpy_winkup_crazy_club:
joined_on: <%= 4.weeks.ago.to_s(:db) %>
club: crazy_club
member_id: 3
favourite: false
type: CurrentMembership

View File

@ -1,6 +1,7 @@
blackbeard:
owner_id: 1
name: blackbeard
essay_id: A Modest Proposal
ashley:
owner_id: 2

View File

@ -52,3 +52,31 @@ eager_other:
title: eager loading with OR'd conditions
body: hello
type: Post
misc_by_bob:
id: 8
author_id: 3
title: misc post by bob
body: hello
type: Post
misc_by_mary:
id: 9
author_id: 2
title: misc post by mary
body: hello
type: Post
other_by_bob:
id: 10
author_id: 3
title: other post by bob
body: hello
type: Post
other_by_mary:
id: 11
author_id: 2
title: other post by mary
body: hello
type: Post

14
activerecord/test/fixtures/ratings.yml vendored Normal file
View File

@ -0,0 +1,14 @@
normal_comment_rating:
id: 1
comment_id: 8
value: 1
special_comment_rating:
id: 2
comment_id: 6
value: 1
sub_special_comment_rating:
id: 3
comment_id: 12
value: 1

View File

@ -26,3 +26,53 @@ godfather:
orphaned:
id: 5
tag_id: 1
misc_post_by_bob:
id: 6
tag_id: 2
taggable_id: 8
taggable_type: Post
misc_post_by_mary:
id: 7
tag_id: 2
taggable_id: 9
taggable_type: Post
misc_by_bob_blue_first:
id: 8
tag_id: 3
taggable_id: 8
taggable_type: Post
comment: first
misc_by_bob_blue_second:
id: 9
tag_id: 3
taggable_id: 8
taggable_type: Post
comment: second
other_by_bob_blue:
id: 10
tag_id: 3
taggable_id: 10
taggable_type: Post
comment: first
other_by_mary_blue:
id: 11
tag_id: 3
taggable_id: 11
taggable_type: Post
comment: first
special_comment_rating:
id: 12
taggable_id: 2
taggable_type: Rating
normal_comment_rating:
id: 13
taggable_id: 1
taggable_type: Rating

View File

@ -4,4 +4,8 @@ general:
misc:
id: 2
name: Misc
name: Misc
blue:
id: 3
name: Blue

View File

@ -94,16 +94,50 @@ class Author < ActiveRecord::Base
has_many :author_favorites
has_many :favorite_authors, :through => :author_favorites, :order => 'name'
has_many :tagging, :through => :posts # through polymorphic has_one
has_many :taggings, :through => :posts, :source => :taggings # through polymorphic has_many
has_many :tags, :through => :posts # through has_many :through
has_many :tagging, :through => :posts
has_many :taggings, :through => :posts
has_many :tags, :through => :posts
has_many :similar_posts, :through => :tags, :source => :tagged_posts, :uniq => true
has_many :distinct_tags, :through => :posts, :source => :tags, :select => "DISTINCT tags.*", :order => "tags.name"
has_many :post_categories, :through => :posts, :source => :categories
has_many :tagging_tags, :through => :taggings, :source => :tag
has_many :tags_with_primary_key, :through => :posts
has_many :books
has_many :subscriptions, :through => :books
has_many :subscribers, :through => :subscriptions, :order => "subscribers.nick" # through has_many :through (on through reflection)
has_many :distinct_subscribers, :through => :subscriptions, :source => :subscriber, :select => "DISTINCT subscribers.*", :order => "subscribers.nick"
has_one :essay, :primary_key => :name, :as => :writer
has_one :essay_category, :through => :essay, :source => :category
has_one :essay_owner, :through => :essay, :source => :owner
belongs_to :author_address, :dependent => :destroy
has_one :essay_2, :primary_key => :name, :class_name => 'Essay', :foreign_key => :author_id
has_one :essay_category_2, :through => :essay_2, :source => :category
has_many :essays, :primary_key => :name, :as => :writer
has_many :essay_categories, :through => :essays, :source => :category
has_many :essay_owners, :through => :essays, :source => :owner
has_many :essays_2, :primary_key => :name, :class_name => 'Essay', :foreign_key => :author_id
has_many :essay_categories_2, :through => :essays_2, :source => :category
belongs_to :owned_essay, :primary_key => :name, :class_name => 'Essay'
has_one :owned_essay_category, :through => :owned_essay, :source => :category
belongs_to :author_address, :dependent => :destroy
belongs_to :author_address_extra, :dependent => :delete, :class_name => "AuthorAddress"
has_many :post_categories, :through => :posts, :source => :categories
has_many :category_post_comments, :through => :categories, :source => :post_comments
has_many :misc_posts, :class_name => 'Post',
:conditions => { :posts => { :title => ['misc post by bob', 'misc post by mary'] } }
has_many :misc_post_first_blue_tags, :through => :misc_posts, :source => :first_blue_tags
has_many :misc_post_first_blue_tags_2, :through => :posts, :source => :first_blue_tags_2,
:conditions => { :posts => { :title => ['misc post by bob', 'misc post by mary'] } }
scope :relation_include_posts, includes(:posts)
scope :relation_include_tags, includes(:tags)

View File

@ -1,4 +1,6 @@
class Book < ActiveRecord::Base
has_many :authors
has_many :citations, :foreign_key => 'book1_id'
has_many :references, :through => :citations, :source => :reference_of, :uniq => true

View File

@ -4,6 +4,8 @@ class Categorization < ActiveRecord::Base
belongs_to :named_category, :class_name => 'Category', :foreign_key => :named_category_name, :primary_key => :name
belongs_to :author
has_many :post_taggings, :through => :author, :source => :taggings
belongs_to :author_using_custom_pk, :class_name => 'Author', :foreign_key => :author_id, :primary_key => :author_address_extra_id
has_many :authors_using_custom_pk, :class_name => 'Author', :foreign_key => :id, :primary_key => :category_id
end

View File

@ -22,6 +22,8 @@ class Category < ActiveRecord::Base
end
has_many :categorizations
has_many :post_comments, :through => :posts, :source => :comments
has_many :authors, :through => :categorizations
has_many :authors_with_select, :through => :categorizations, :source => :author, :select => 'authors.*, categorizations.post_id'

View File

@ -5,6 +5,7 @@ class Club < ActiveRecord::Base
has_many :current_memberships
has_one :sponsor
has_one :sponsored_member, :through => :sponsor, :source => :sponsorable, :source_type => "Member"
belongs_to :category
private

View File

@ -8,6 +8,7 @@ class Comment < ActiveRecord::Base
:conditions => { "posts.author_id" => 1 }
belongs_to :post, :counter_cache => true
has_many :ratings
def self.what_are_you
'a comment...'

View File

@ -1,3 +1,5 @@
class Essay < ActiveRecord::Base
belongs_to :writer, :primary_key => :name, :polymorphic => true
belongs_to :category, :primary_key => :name
has_one :owner, :primary_key => :name
end

View File

@ -2,4 +2,6 @@ class Job < ActiveRecord::Base
has_many :references
has_many :people, :through => :references
belongs_to :ideal_reference, :class_name => 'Reference'
has_many :agents, :through => :people
end

View File

@ -11,6 +11,17 @@ class Member < ActiveRecord::Base
has_one :organization, :through => :member_detail
belongs_to :member_type
has_many :nested_member_types, :through => :member_detail, :source => :member_type
has_one :nested_member_type, :through => :member_detail, :source => :member_type
has_many :nested_sponsors, :through => :sponsor_club, :source => :sponsor
has_one :nested_sponsor, :through => :sponsor_club, :source => :sponsor
has_many :organization_member_details, :through => :member_detail
has_many :organization_member_details_2, :through => :organization, :source => :member_details
has_one :club_category, :through => :club, :source => :category
has_many :current_memberships
has_one :club_through_many, :through => :current_memberships, :source => :club

View File

@ -2,4 +2,6 @@ class MemberDetail < ActiveRecord::Base
belongs_to :member
belongs_to :organization
has_one :member_type, :through => :member
has_many :organization_member_details, :through => :organization, :source => :member_details
end

View File

@ -2,5 +2,11 @@ class Organization < ActiveRecord::Base
has_many :member_details
has_many :members, :through => :member_details
has_many :authors, :primary_key => :name
has_many :author_essay_categories, :through => :authors, :source => :essay_categories
has_one :author, :primary_key => :name
has_one :author_owned_essay_category, :through => :author, :source => :owned_essay_category
scope :clubs, { :from => 'clubs' }
end
end

View File

@ -21,6 +21,9 @@ class Person < ActiveRecord::Base
has_many :agents_of_agents, :through => :agents, :source => :agents
belongs_to :number1_fan, :class_name => 'Person'
has_many :agents_posts, :through => :agents, :source => :posts
has_many :agents_posts_authors, :through => :agents_posts, :source => :author
scope :males, :conditions => { :gender => 'M' }
scope :females, :conditions => { :gender => 'F' }
end

View File

@ -49,6 +49,9 @@ class Post < ActiveRecord::Base
has_many :special_comments
has_many :nonexistant_comments, :class_name => 'Comment', :conditions => 'comments.id < 0'
has_many :special_comments_ratings, :through => :special_comments, :source => :ratings
has_many :special_comments_ratings_taggings, :through => :special_comments_ratings, :source => :taggings
has_and_belongs_to_many :categories
has_and_belongs_to_many :special_categories, :join_table => "categories_posts", :association_foreign_key => 'category_id'
@ -70,11 +73,17 @@ class Post < ActiveRecord::Base
has_many :tags_with_destroy, :through => :taggings, :source => :tag, :dependent => :destroy
has_many :tags_with_nullify, :through => :taggings, :source => :tag, :dependent => :nullify
has_many :misc_tags, :through => :taggings, :source => :tag, :conditions => "tags.name = 'Misc'"
has_many :misc_tags, :through => :taggings, :source => :tag, :conditions => { :tags => { :name => 'Misc' } }
has_many :funky_tags, :through => :taggings, :source => :tag
has_many :super_tags, :through => :taggings
has_many :tags_with_primary_key, :through => :taggings, :source => :tag_with_primary_key
has_one :tagging, :as => :taggable
has_many :first_taggings, :as => :taggable, :class_name => 'Tagging', :conditions => { :taggings => { :comment => 'first' } }
has_many :first_blue_tags, :through => :first_taggings, :source => :tag, :conditions => { :tags => { :name => 'Blue' } }
has_many :first_blue_tags_2, :through => :taggings, :source => :blue_tag, :conditions => { :taggings => { :comment => 'first' } }
has_many :invalid_taggings, :as => :taggable, :class_name => "Tagging", :conditions => 'taggings.id < 0'
has_many :invalid_tags, :through => :invalid_taggings, :source => :tag

View File

@ -0,0 +1,4 @@
class Rating < ActiveRecord::Base
belongs_to :comment
has_many :taggings, :as => :taggable
end

View File

@ -2,6 +2,8 @@ class Reference < ActiveRecord::Base
belongs_to :person
belongs_to :job
has_many :agents_posts_authors, :through => :person
class << self
attr_accessor :make_comments
end

View File

@ -6,6 +6,8 @@ class Tagging < ActiveRecord::Base
belongs_to :tag, :include => :tagging
belongs_to :super_tag, :class_name => 'Tag', :foreign_key => 'super_tag_id'
belongs_to :invalid_tag, :class_name => 'Tag', :foreign_key => 'tag_id'
belongs_to :blue_tag, :class_name => 'Tag', :foreign_key => :tag_id, :conditions => { :tags => { :name => 'Blue' } }
belongs_to :tag_with_primary_key, :class_name => 'Tag', :foreign_key => :tag_id, :primary_key => :custom_primary_key
belongs_to :interpolated_tag, :class_name => 'Tag', :foreign_key => :tag_id, :conditions => proc { "1 = #{1}" }
belongs_to :taggable, :polymorphic => true, :counter_cache => true
has_many :things, :through => :taggable

View File

@ -49,6 +49,8 @@ ActiveRecord::Schema.define do
t.string :name, :null => false
t.integer :author_address_id
t.integer :author_address_extra_id
t.string :organization_id
t.string :owned_essay_id
end
create_table :author_addresses, :force => true do |t|
@ -75,6 +77,7 @@ ActiveRecord::Schema.define do
end
create_table :books, :force => true do |t|
t.integer :author_id
t.column :name, :string
end
@ -123,6 +126,7 @@ ActiveRecord::Schema.define do
create_table :clubs, :force => true do |t|
t.string :name
t.integer :category_id
end
create_table :collections, :force => true do |t|
@ -216,6 +220,8 @@ ActiveRecord::Schema.define do
t.string :name
t.string :writer_id
t.string :writer_type
t.string :category_id
t.string :author_id
end
create_table :events, :force => true do |t|
@ -393,6 +399,7 @@ ActiveRecord::Schema.define do
t.string :name
t.column :updated_at, :datetime
t.column :happy_at, :datetime
t.string :essay_id
end
create_table :paint_colors, :force => true do |t|
@ -482,6 +489,11 @@ ActiveRecord::Schema.define do
t.string :type
end
create_table :ratings, :force => true do |t|
t.integer :comment_id
t.integer :value
end
create_table :readers, :force => true do |t|
t.integer :post_id, :null => false
t.integer :person_id, :null => false
@ -553,6 +565,7 @@ ActiveRecord::Schema.define do
t.column :super_tag_id, :integer
t.column :taggable_type, :string
t.column :taggable_id, :integer
t.string :comment
end
create_table :tasks, :force => true do |t|
@ -677,6 +690,9 @@ ActiveRecord::Schema.define do
t.integer :molecule_id
t.string :name
end
create_table :weirds, :force => true do |t|
t.string 'a$b'
end
except 'SQLite' do
# fk_test_has_fk should be before fk_test_has_pk

View File

@ -29,7 +29,7 @@ module ActiveSupport
def convert_json_to_yaml(json) #:nodoc:
require 'strscan' unless defined? ::StringScanner
scanner, quoting, marks, pos, times = ::StringScanner.new(json), false, [], nil, []
while scanner.scan_until(/(\\['"]|['":,\\]|\\.)/)
while scanner.scan_until(/(\\['"]|['":,\\]|\\.|[\]])/)
case char = scanner[1]
when '"', "'"
if !quoting
@ -43,7 +43,7 @@ module ActiveSupport
end
quoting = false
end
when ":",","
when ":",",", "]"
marks << scanner.pos - 1 unless quoting
when "\\"
scanner.skip(/\\/)
@ -70,9 +70,11 @@ module ActiveSupport
left_pos.each_with_index do |left, i|
scanner.pos = left.succ
chunk = scanner.peek(right_pos[i] - scanner.pos + 1)
# overwrite the quotes found around the dates with spaces
while times.size > 0 && times[0] <= right_pos[i]
chunk.insert(times.shift - scanner.pos - 1, '! ')
if ActiveSupport.parse_json_times
# overwrite the quotes found around the dates with spaces
while times.size > 0 && times[0] <= right_pos[i]
chunk.insert(times.shift - scanner.pos - 1, '! ')
end
end
chunk.gsub!(/\\([\\\/]|u[[:xdigit:]]{4})/) do
ustr = $1

View File

@ -17,6 +17,8 @@ class TestJSONDecoding < ActiveSupport::TestCase
%({"matzue": "松江", "asakusa": "浅草"}) => {"matzue" => "松江", "asakusa" => "浅草"},
%({"a": "2007-01-01"}) => {'a' => Date.new(2007, 1, 1)},
%({"a": "2007-01-01 01:12:34 Z"}) => {'a' => Time.utc(2007, 1, 1, 1, 12, 34)},
%(["2007-01-01 01:12:34 Z"]) => [Time.utc(2007, 1, 1, 1, 12, 34)],
%(["2007-01-01 01:12:34 Z", "2007-01-01 01:12:35 Z"]) => [Time.utc(2007, 1, 1, 1, 12, 34), Time.utc(2007, 1, 1, 1, 12, 35)],
# no time zone
%({"a": "2007-01-01 01:12:34"}) => {'a' => "2007-01-01 01:12:34"},
# invalid date
@ -72,13 +74,11 @@ class TestJSONDecoding < ActiveSupport::TestCase
end
end
end
end
if backends.include?("JSONGem")
test "json decodes time json with time parsing disabled" do
test "json decodes time json with time parsing disabled with the #{backend} backend" do
ActiveSupport.parse_json_times = false
expected = {"a" => "2007-01-01 01:12:34 Z"}
ActiveSupport::JSON.with_backend "JSONGem" do
ActiveSupport::JSON.with_backend backend do
assert_equal expected, ActiveSupport::JSON.decode(%({"a": "2007-01-01 01:12:34 Z"}))
end
end

View File

@ -747,6 +747,22 @@ h4. Specifying Conditions on Eager Loaded Associations
Even though Active Record lets you specify conditions on the eager loaded associations just like +joins+, the recommended way is to use "joins":#joining-tables instead.
However if you must do this, you may use +where+ as you would normally.
<ruby>
Post.includes(:comments).where("comments.visible", true)
</ruby>
This would generate a query which contains a +LEFT OUTER JOIN+ whereas the +joins+ method would generate one using the +INNER JOIN+ function instead.
<ruby>
SELECT "posts"."id" AS t0_r0, ... "comments"."updated_at" AS t1_r5 FROM "posts" LEFT OUTER JOIN "comments" ON "comments"."post_id" = "posts"."id" WHERE (comments.visible)
</ruby>
If there was no +where+ condition, this would generate the normal set of two queries.
If, in the case of this +includes+ query, there were no comments for any posts, all the posts would still be loaded. By using +joins+ (an INNER JOIN), the join conditions *must* match, otherwise no records will be returned.
h3. Scopes
Scoping allows you to specify commonly-used ARel queries which can be referenced as method calls on the association objects or models. With these scopes, you can use every method previously covered such as +where+, +joins+ and +includes+. All scope methods will return an +ActiveRecord::Relation+ object which will allow for further methods (such as other scopes) to be called on it.

View File

@ -65,7 +65,7 @@ end
If you want a more complicated expiration scheme, you can use cache sweepers to expire cached objects when things change. This is covered in the section on Sweepers.
Note: Page caching ignores all parameters. For example +/products?page=1+ will be written out to the filesystem as +products.html+ with no reference to the +page+ parameter. Thus, if someone requests +/products?page=2+ later, they will get the cached first page. Be careful when page caching GET parameters in the URL!
NOTE: Page caching ignores all parameters. For example +/products?page=1+ will be written out to the filesystem as +products.html+ with no reference to the +page+ parameter. Thus, if someone requests +/products?page=2+ later, they will get the cached first page. Be careful when page caching GET parameters in the URL!
INFO: Page caching runs in an after filter. Thus, invalid requests won't generate spurious cache entries as long as you halt them. Typically, a redirection in some before filter that checks request preconditions does the job.

View File

@ -371,7 +371,7 @@ Now create a ticket with your patch. Go to the "new ticket":http://rails.lightho
h4. Get Some Feedback
Now you need to get other people to look at your patch, just as you've looked at other people's patches. You can use the rubyonrails-core mailing list or the #rails-contrib channel on IRC freenode for this. You might also try just talking to Rails developers that you know.
Now you need to get other people to look at your patch, just as you've looked at other people's patches. You can use the "rubyonrails-core mailing list":http://groups.google.com/group/rubyonrails-core/ or the #rails-contrib channel on IRC freenode for this. You might also try just talking to Rails developers that you know.
h4. Iterate as Necessary

View File

@ -149,7 +149,7 @@ Usually run this as the root user:
# gem install rails
</shell>
TIP. If you're working on Windows, you should be aware that the vast majority of Rails development is done in Unix environments. While Ruby and Rails themselves install easily using for example "Ruby Installer":http://rubyinstaller.org/, the supporting ecosystem often assumes you are able to build C-based rubygems and work in a command window. If at all possible, we suggest that you install a Linux virtual machine and use that for Rails development, instead of using Windows.
TIP. If you're working on Windows, you can quickly install Ruby and Rails with "Rails Installer":http://railsinstaller.org.
h4. Creating the Blog Application

View File

@ -62,7 +62,7 @@ task :default => :test
end
def generate_test_dummy(force = false)
opts = (options || {}).slice(:skip_active_record, :skip_javascript, :database, :javascript)
opts = (options || {}).slice(:skip_active_record, :skip_javascript, :database, :javascript, :quiet, :pretend, :force, :skip)
opts[:force] = force
invoke Rails::Generators::AppGenerator,

View File

@ -31,26 +31,34 @@ module Rails
app_generators(&block)
end
# First configurable block to run. Called before any initializers are run.
def before_configuration(&block)
ActiveSupport.on_load(:before_configuration, :yield => true, &block)
end
# Third configurable block to run. Does not run if config.cache_classes
# set to false.
def before_eager_load(&block)
ActiveSupport.on_load(:before_eager_load, :yield => true, &block)
end
# Second configurable block to run. Called before frameworks initialize.
def before_initialize(&block)
ActiveSupport.on_load(:before_initialize, :yield => true, &block)
end
# Last configurable block to run. Called after frameworks initialize.
def after_initialize(&block)
ActiveSupport.on_load(:after_initialize, :yield => true, &block)
end
# Array of callbacks defined by #to_prepare.
def to_prepare_blocks
@@to_prepare_blocks ||= []
end
# Defines generic callbacks to run before #after_initialize. Useful for
# Rails::Railtie subclasses.
def to_prepare(&blk)
to_prepare_blocks << blk if blk
end

View File

@ -26,8 +26,7 @@ module SharedGeneratorTests
def test_plugin_new_generate_pretend
run_generator ["testapp", "--pretend"]
default_files.each{ |path| assert_no_file path }
default_files.each{ |path| assert_no_file File.join("testapp",path) }
end
def test_invalid_database_option_raises_an_error