Properly support conditions on any of the reflections involved in a nested through association
This commit is contained in:
parent
78b8c51cb3
commit
9ec0734874
@ -2164,8 +2164,10 @@ def join_to(relation)
|
|||||||
chain = through_reflection_chain.reverse
|
chain = through_reflection_chain.reverse
|
||||||
|
|
||||||
foreign_table = parent_table
|
foreign_table = parent_table
|
||||||
|
index = 0
|
||||||
|
|
||||||
chain.zip(@tables).each do |reflection, table|
|
chain.each do |reflection|
|
||||||
|
table = @tables[index]
|
||||||
conditions = []
|
conditions = []
|
||||||
|
|
||||||
if reflection.source_reflection.nil?
|
if reflection.source_reflection.nil?
|
||||||
@ -2234,13 +2236,14 @@ def join_to(relation)
|
|||||||
|
|
||||||
conditions << table[key].eq(foreign_table[foreign_key])
|
conditions << table[key].eq(foreign_table[foreign_key])
|
||||||
|
|
||||||
conditions << reflection_conditions(reflection, table)
|
conditions << reflection_conditions(index, table)
|
||||||
conditions << sti_conditions(reflection, table)
|
conditions << sti_conditions(reflection, table)
|
||||||
|
|
||||||
relation = relation.join(table, join_type).on(*conditions.compact)
|
relation = relation.join(table, join_type).on(*conditions.flatten.compact)
|
||||||
|
|
||||||
# The current table in this iteration becomes the foreign table in the next
|
# The current table in this iteration becomes the foreign table in the next
|
||||||
foreign_table = table
|
foreign_table = table
|
||||||
|
index += 1
|
||||||
end
|
end
|
||||||
|
|
||||||
relation
|
relation
|
||||||
@ -2325,10 +2328,10 @@ def setup_tables
|
|||||||
@tables
|
@tables
|
||||||
end
|
end
|
||||||
|
|
||||||
def reflection_conditions(reflection, table)
|
def reflection_conditions(index, table)
|
||||||
if reflection.options[:conditions]
|
@reflection.through_conditions.reverse[index].map do |condition|
|
||||||
Arel.sql(interpolate_sql(sanitize_sql(
|
Arel.sql(interpolate_sql(sanitize_sql(
|
||||||
reflection.options[:conditions],
|
condition,
|
||||||
table.table_alias || table.name
|
table.table_alias || table.name
|
||||||
)))
|
)))
|
||||||
end
|
end
|
||||||
|
@ -24,7 +24,6 @@ def construct_create_scope
|
|||||||
end
|
end
|
||||||
|
|
||||||
# Build SQL conditions from attributes, qualified by table name.
|
# Build SQL conditions from attributes, qualified by table name.
|
||||||
# TODO: Conditions on joins
|
|
||||||
def construct_conditions
|
def construct_conditions
|
||||||
reflection = @reflection.through_reflection_chain.last
|
reflection = @reflection.through_reflection_chain.last
|
||||||
|
|
||||||
@ -34,11 +33,12 @@ def construct_conditions
|
|||||||
table_alias = table_aliases[reflection]
|
table_alias = table_aliases[reflection]
|
||||||
end
|
end
|
||||||
|
|
||||||
conditions = construct_quoted_owner_attributes(reflection).map do |attr, value|
|
parts = construct_quoted_owner_attributes(reflection).map do |attr, value|
|
||||||
"#{table_alias}.#{attr} = #{value}"
|
"#{table_alias}.#{attr} = #{value}"
|
||||||
end
|
end
|
||||||
conditions << sql_conditions if sql_conditions
|
parts += reflection_conditions(0)
|
||||||
"(" + conditions.join(') AND (') + ")"
|
|
||||||
|
"(" + parts.join(') AND (') + ")"
|
||||||
end
|
end
|
||||||
|
|
||||||
# Associate attributes pointing to owner, quoted.
|
# Associate attributes pointing to owner, quoted.
|
||||||
@ -55,23 +55,21 @@ def construct_quoted_owner_attributes(reflection)
|
|||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
def construct_from
|
|
||||||
@reflection.table_name
|
|
||||||
end
|
|
||||||
|
|
||||||
def construct_select(custom_select = nil)
|
def construct_select(custom_select = nil)
|
||||||
distinct = "DISTINCT " if @reflection.options[:uniq]
|
distinct = "DISTINCT " if @reflection.options[:uniq]
|
||||||
selected = custom_select || @reflection.options[:select] || "#{distinct}#{@reflection.quoted_table_name}.*"
|
selected = custom_select || @reflection.options[:select] || "#{distinct}#{@reflection.quoted_table_name}.*"
|
||||||
end
|
end
|
||||||
|
|
||||||
def construct_joins(custom_joins = nil)
|
def construct_joins(custom_joins = nil)
|
||||||
# p @reflection.through_reflection_chain
|
# TODO: Remove this at the end
|
||||||
|
#p @reflection.through_reflection_chain
|
||||||
|
#p @reflection.through_conditions
|
||||||
|
|
||||||
"#{construct_through_joins} #{@reflection.options[:joins]} #{custom_joins}"
|
"#{construct_through_joins} #{@reflection.options[:joins]} #{custom_joins}"
|
||||||
end
|
end
|
||||||
|
|
||||||
def construct_through_joins
|
def construct_through_joins
|
||||||
joins = []
|
joins, right_index = [], 1
|
||||||
|
|
||||||
# Iterate over each pair in the through reflection chain, joining them together
|
# Iterate over each pair in the through reflection chain, joining them together
|
||||||
@reflection.through_reflection_chain.each_cons(2) do |left, right|
|
@reflection.through_reflection_chain.each_cons(2) do |left, right|
|
||||||
@ -86,20 +84,23 @@ def construct_through_joins
|
|||||||
joins << inner_join_sql(
|
joins << inner_join_sql(
|
||||||
right_table_and_alias,
|
right_table_and_alias,
|
||||||
table_aliases[left], left.klass.primary_key,
|
table_aliases[left], left.klass.primary_key,
|
||||||
table_aliases[right], left.primary_key_name
|
table_aliases[right], left.primary_key_name,
|
||||||
|
reflection_conditions(right_index)
|
||||||
)
|
)
|
||||||
when :has_many, :has_one
|
when :has_many, :has_one
|
||||||
joins << inner_join_sql(
|
joins << inner_join_sql(
|
||||||
right_table_and_alias,
|
right_table_and_alias,
|
||||||
table_aliases[left], left.primary_key_name,
|
table_aliases[left], left.primary_key_name,
|
||||||
table_aliases[right], right.klass.primary_key,
|
table_aliases[right], right.klass.primary_key,
|
||||||
polymorphic_conditions(left, left)
|
polymorphic_conditions(left, left),
|
||||||
|
reflection_conditions(right_index)
|
||||||
)
|
)
|
||||||
when :has_and_belongs_to_many
|
when :has_and_belongs_to_many
|
||||||
joins << inner_join_sql(
|
joins << inner_join_sql(
|
||||||
right_table_and_alias,
|
right_table_and_alias,
|
||||||
table_aliases[left].first, left.primary_key_name,
|
table_aliases[left].first, left.primary_key_name,
|
||||||
table_aliases[right], right.klass.primary_key
|
table_aliases[right], right.klass.primary_key,
|
||||||
|
reflection_conditions(right_index)
|
||||||
)
|
)
|
||||||
end
|
end
|
||||||
else
|
else
|
||||||
@ -109,7 +110,8 @@ def construct_through_joins
|
|||||||
right_table_and_alias,
|
right_table_and_alias,
|
||||||
table_aliases[left], left.klass.primary_key,
|
table_aliases[left], left.klass.primary_key,
|
||||||
table_aliases[right], left.source_reflection.primary_key_name,
|
table_aliases[right], left.source_reflection.primary_key_name,
|
||||||
source_type_conditions(left)
|
source_type_conditions(left),
|
||||||
|
reflection_conditions(right_index)
|
||||||
)
|
)
|
||||||
when :has_many, :has_one
|
when :has_many, :has_one
|
||||||
if right.macro == :has_and_belongs_to_many
|
if right.macro == :has_and_belongs_to_many
|
||||||
@ -123,7 +125,8 @@ def construct_through_joins
|
|||||||
right_table_and_alias,
|
right_table_and_alias,
|
||||||
table_aliases[left], left.source_reflection.primary_key_name,
|
table_aliases[left], left.source_reflection.primary_key_name,
|
||||||
right_table, right.klass.primary_key,
|
right_table, right.klass.primary_key,
|
||||||
polymorphic_conditions(left, left.source_reflection)
|
polymorphic_conditions(left, left.source_reflection),
|
||||||
|
reflection_conditions(right_index)
|
||||||
)
|
)
|
||||||
|
|
||||||
if right.macro == :has_and_belongs_to_many
|
if right.macro == :has_and_belongs_to_many
|
||||||
@ -151,10 +154,13 @@ def construct_through_joins
|
|||||||
joins << inner_join_sql(
|
joins << inner_join_sql(
|
||||||
right_table_and_alias,
|
right_table_and_alias,
|
||||||
join_table, left.source_reflection.primary_key_name,
|
join_table, left.source_reflection.primary_key_name,
|
||||||
table_aliases[right], right.klass.primary_key
|
table_aliases[right], right.klass.primary_key,
|
||||||
|
reflection_conditions(right_index)
|
||||||
)
|
)
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
|
right_index += 1
|
||||||
end
|
end
|
||||||
|
|
||||||
joins.join(" ")
|
joins.join(" ")
|
||||||
@ -206,18 +212,45 @@ def table_name_and_alias(table_name, table_alias)
|
|||||||
"#{table_name} #{table_alias if table_alias != table_name}".strip
|
"#{table_name} #{table_alias if table_alias != table_name}".strip
|
||||||
end
|
end
|
||||||
|
|
||||||
def inner_join_sql(table, on_left_table, on_left_key, on_right_table, on_right_key, conds = nil)
|
def inner_join_sql(table, on_left_table, on_left_key, on_right_table, on_right_key, *conditions)
|
||||||
"INNER JOIN %s ON %s.%s = %s.%s %s" % [
|
conditions << "#{on_left_table}.#{on_left_key} = #{on_right_table}.#{on_right_key}"
|
||||||
table,
|
conditions = conditions.flatten.compact
|
||||||
on_left_table, on_left_key,
|
conditions = conditions.map { |sql| "(#{sql})" } * ' AND '
|
||||||
on_right_table, on_right_key,
|
|
||||||
conds
|
"INNER JOIN #{table} ON #{conditions}"
|
||||||
]
|
end
|
||||||
|
|
||||||
|
def reflection_conditions(index)
|
||||||
|
reflection = @reflection.through_reflection_chain[index]
|
||||||
|
reflection_conditions = @reflection.through_conditions[index]
|
||||||
|
|
||||||
|
conditions = []
|
||||||
|
|
||||||
|
if reflection.options[:as].nil? && # reflection.klass is a Module if :as is used
|
||||||
|
reflection.klass.finder_needs_type_condition?
|
||||||
|
conditions << reflection.klass.send(:type_condition).to_sql
|
||||||
|
end
|
||||||
|
|
||||||
|
reflection_conditions.each do |condition|
|
||||||
|
sanitized_condition = reflection.klass.send(:sanitize_sql, condition)
|
||||||
|
interpolated_condition = interpolate_sql(sanitized_condition)
|
||||||
|
|
||||||
|
if condition.is_a?(Hash)
|
||||||
|
interpolated_condition.gsub!(
|
||||||
|
@reflection.quoted_table_name,
|
||||||
|
reflection.quoted_table_name
|
||||||
|
)
|
||||||
|
end
|
||||||
|
|
||||||
|
conditions << interpolated_condition
|
||||||
|
end
|
||||||
|
|
||||||
|
conditions
|
||||||
end
|
end
|
||||||
|
|
||||||
def polymorphic_conditions(reflection, polymorphic_reflection)
|
def polymorphic_conditions(reflection, polymorphic_reflection)
|
||||||
if polymorphic_reflection.options[:as]
|
if polymorphic_reflection.options[:as]
|
||||||
"AND %s.%s = %s" % [
|
"%s.%s = %s" % [
|
||||||
table_aliases[reflection], "#{polymorphic_reflection.options[:as]}_type",
|
table_aliases[reflection], "#{polymorphic_reflection.options[:as]}_type",
|
||||||
@owner.class.quote_value(polymorphic_reflection.active_record.base_class.name)
|
@owner.class.quote_value(polymorphic_reflection.active_record.base_class.name)
|
||||||
]
|
]
|
||||||
@ -226,7 +259,7 @@ def polymorphic_conditions(reflection, polymorphic_reflection)
|
|||||||
|
|
||||||
def source_type_conditions(reflection)
|
def source_type_conditions(reflection)
|
||||||
if reflection.options[:source_type]
|
if reflection.options[:source_type]
|
||||||
"AND %s.%s = %s" % [
|
"%s.%s = %s" % [
|
||||||
table_aliases[reflection.through_reflection],
|
table_aliases[reflection.through_reflection],
|
||||||
reflection.source_reflection.options[:foreign_type].to_s,
|
reflection.source_reflection.options[:foreign_type].to_s,
|
||||||
@owner.class.quote_value(reflection.options[:source_type])
|
@owner.class.quote_value(reflection.options[:source_type])
|
||||||
@ -245,6 +278,8 @@ def construct_owner_attributes(reflection)
|
|||||||
end
|
end
|
||||||
|
|
||||||
# Construct attributes for :through pointing to owner and associate.
|
# Construct attributes for :through pointing to owner and associate.
|
||||||
|
# This method is used when adding records to the association. Since this only makes sense for
|
||||||
|
# non-nested through associations, that's the only case we have to worry about here.
|
||||||
def construct_join_attributes(associate)
|
def construct_join_attributes(associate)
|
||||||
# TODO: revisit this to allow it for deletion, supposing dependent option is supported
|
# TODO: revisit this to allow it for deletion, supposing dependent option is supported
|
||||||
raise ActiveRecord::HasManyThroughCantAssociateThroughHasOneOrManyReflection.new(@owner, @reflection) if [:has_one, :has_many].include?(@reflection.source_reflection.macro)
|
raise ActiveRecord::HasManyThroughCantAssociateThroughHasOneOrManyReflection.new(@owner, @reflection) if [:has_one, :has_many].include?(@reflection.source_reflection.macro)
|
||||||
@ -261,48 +296,6 @@ def construct_join_attributes(associate)
|
|||||||
|
|
||||||
join_attributes
|
join_attributes
|
||||||
end
|
end
|
||||||
|
|
||||||
def conditions
|
|
||||||
@conditions = build_conditions unless defined?(@conditions)
|
|
||||||
@conditions
|
|
||||||
end
|
|
||||||
|
|
||||||
def build_conditions
|
|
||||||
association_conditions = @reflection.options[:conditions]
|
|
||||||
through_conditions = build_through_conditions
|
|
||||||
source_conditions = @reflection.source_reflection.options[:conditions]
|
|
||||||
uses_sti = !@reflection.through_reflection.klass.descends_from_active_record?
|
|
||||||
|
|
||||||
if association_conditions || through_conditions || source_conditions || uses_sti
|
|
||||||
all = []
|
|
||||||
|
|
||||||
[association_conditions, source_conditions].each do |conditions|
|
|
||||||
all << interpolate_sql(sanitize_sql(conditions)) if conditions
|
|
||||||
end
|
|
||||||
|
|
||||||
all << through_conditions if through_conditions
|
|
||||||
all << build_sti_condition if uses_sti
|
|
||||||
|
|
||||||
all.map { |sql| "(#{sql})" } * ' AND '
|
|
||||||
end
|
|
||||||
end
|
|
||||||
|
|
||||||
def build_through_conditions
|
|
||||||
conditions = @reflection.through_reflection.options[:conditions]
|
|
||||||
if conditions.is_a?(Hash)
|
|
||||||
interpolate_sql(@reflection.through_reflection.klass.send(:sanitize_sql, conditions)).gsub(
|
|
||||||
@reflection.quoted_table_name,
|
|
||||||
@reflection.through_reflection.quoted_table_name)
|
|
||||||
elsif conditions
|
|
||||||
interpolate_sql(sanitize_sql(conditions))
|
|
||||||
end
|
|
||||||
end
|
|
||||||
|
|
||||||
def build_sti_condition
|
|
||||||
@reflection.through_reflection.klass.send(:type_condition).to_sql
|
|
||||||
end
|
|
||||||
|
|
||||||
alias_method :sql_conditions, :conditions
|
|
||||||
|
|
||||||
def ensure_not_nested
|
def ensure_not_nested
|
||||||
if @reflection.nested?
|
if @reflection.nested?
|
||||||
|
@ -253,6 +253,10 @@ def through_reflection
|
|||||||
def through_reflection_chain
|
def through_reflection_chain
|
||||||
[self]
|
[self]
|
||||||
end
|
end
|
||||||
|
|
||||||
|
def through_conditions
|
||||||
|
[Array.wrap(options[:conditions])]
|
||||||
|
end
|
||||||
|
|
||||||
def through_reflection_primary_key_name
|
def through_reflection_primary_key_name
|
||||||
end
|
end
|
||||||
@ -378,9 +382,9 @@ def through_reflection
|
|||||||
# TODO: Documentation
|
# TODO: Documentation
|
||||||
def through_reflection_chain
|
def through_reflection_chain
|
||||||
@through_reflection_chain ||= begin
|
@through_reflection_chain ||= begin
|
||||||
if source_reflection.through_reflection
|
if source_reflection.source_reflection
|
||||||
# If the source reflection goes through another reflection, then the chain must start
|
# If the source reflection has its own source reflection, then the chain must start
|
||||||
# by getting us to the source reflection.
|
# by getting us to that source reflection.
|
||||||
chain = source_reflection.through_reflection_chain
|
chain = source_reflection.through_reflection_chain
|
||||||
else
|
else
|
||||||
# If the source reflection does not go through another reflection, then we can get
|
# If the source reflection does not go through another reflection, then we can get
|
||||||
@ -396,6 +400,49 @@ def through_reflection_chain
|
|||||||
end
|
end
|
||||||
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 through_reflection_chain. So this method
|
||||||
|
# creates an array of conditions corresponding to the through_reflection_chain. Each item in
|
||||||
|
# the through_conditions array corresponds to an item in the through_reflection_chain, and is
|
||||||
|
# itself an array of conditions from an arbitrary number of relevant reflections.
|
||||||
|
def through_conditions
|
||||||
|
@through_conditions ||= begin
|
||||||
|
# Initialize the first item - which corresponds to this reflection - either by recursing
|
||||||
|
# into the souce reflection (if it is itself a through reflection), or by grabbing the
|
||||||
|
# source reflection conditions.
|
||||||
|
if source_reflection.source_reflection
|
||||||
|
conditions = source_reflection.through_conditions
|
||||||
|
else
|
||||||
|
conditions = [Array.wrap(source_reflection.options[:conditions])]
|
||||||
|
end
|
||||||
|
|
||||||
|
# Add to it the conditions from this reflection if necessary.
|
||||||
|
conditions.first << options[:conditions] if options[:conditions]
|
||||||
|
|
||||||
|
# Recursively fill out the rest of the array from the through reflection
|
||||||
|
conditions += through_reflection.through_conditions
|
||||||
|
|
||||||
|
# And return
|
||||||
|
conditions
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
def nested?
|
def nested?
|
||||||
through_reflection_chain.length > 2
|
through_reflection_chain.length > 2
|
||||||
end
|
end
|
||||||
|
@ -15,7 +15,7 @@ def test_eager_association_loading_with_cascaded_two_levels
|
|||||||
authors = Author.find(:all, :include=>{:posts=>:comments}, :order=>"authors.id")
|
authors = Author.find(:all, :include=>{:posts=>:comments}, :order=>"authors.id")
|
||||||
assert_equal 3, authors.size
|
assert_equal 3, authors.size
|
||||||
assert_equal 5, authors[0].posts.size
|
assert_equal 5, authors[0].posts.size
|
||||||
assert_equal 2, 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 10, authors[0].posts.collect{|post| post.comments.size }.inject(0){|sum,i| sum+i}
|
||||||
end
|
end
|
||||||
|
|
||||||
@ -23,7 +23,7 @@ def test_eager_association_loading_with_cascaded_two_levels_and_one_level
|
|||||||
authors = Author.find(:all, :include=>[{:posts=>:comments}, :categorizations], :order=>"authors.id")
|
authors = Author.find(:all, :include=>[{:posts=>:comments}, :categorizations], :order=>"authors.id")
|
||||||
assert_equal 3, authors.size
|
assert_equal 3, authors.size
|
||||||
assert_equal 5, authors[0].posts.size
|
assert_equal 5, authors[0].posts.size
|
||||||
assert_equal 2, 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 10, authors[0].posts.collect{|post| post.comments.size }.inject(0){|sum,i| sum+i}
|
||||||
assert_equal 1, authors[0].categorizations.size
|
assert_equal 1, authors[0].categorizations.size
|
||||||
assert_equal 2, authors[1].categorizations.size
|
assert_equal 2, authors[1].categorizations.size
|
||||||
@ -56,7 +56,7 @@ def test_eager_association_loading_with_cascaded_two_levels_with_two_has_many_as
|
|||||||
authors = Author.find(:all, :include=>{:posts=>[:comments, :categorizations]}, :order=>"authors.id")
|
authors = Author.find(:all, :include=>{:posts=>[:comments, :categorizations]}, :order=>"authors.id")
|
||||||
assert_equal 3, authors.size
|
assert_equal 3, authors.size
|
||||||
assert_equal 5, authors[0].posts.size
|
assert_equal 5, authors[0].posts.size
|
||||||
assert_equal 2, 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 10, authors[0].posts.collect{|post| post.comments.size }.inject(0){|sum,i| sum+i}
|
||||||
end
|
end
|
||||||
|
|
||||||
|
@ -53,8 +53,8 @@ def test_loading_conditions_with_or
|
|||||||
|
|
||||||
def test_with_ordering
|
def test_with_ordering
|
||||||
list = Post.find(:all, :include => :comments, :order => "posts.id DESC")
|
list = Post.find(:all, :include => :comments, :order => "posts.id DESC")
|
||||||
[:misc_by_mary, :misc_by_bob, :eager_other, :sti_habtm, :sti_post_and_comments,
|
[:other_by_mary, :other_by_bob, :misc_by_mary, :misc_by_bob, :eager_other,
|
||||||
:sti_comments, :authorless, :thinking, :welcome
|
:sti_habtm, :sti_post_and_comments, :sti_comments, :authorless, :thinking, :welcome
|
||||||
].each_with_index do |post, index|
|
].each_with_index do |post, index|
|
||||||
assert_equal posts(post), list[index]
|
assert_equal posts(post), list[index]
|
||||||
end
|
end
|
||||||
|
@ -92,8 +92,6 @@ def test_has_many_through_has_many_through_with_has_many_source_reflection
|
|||||||
assert_no_queries do
|
assert_no_queries do
|
||||||
assert_equal [luke, david, david], authors.first.subscribers.sort_by(&:nick)
|
assert_equal [luke, david, david], authors.first.subscribers.sort_by(&:nick)
|
||||||
end
|
end
|
||||||
|
|
||||||
# TODO: Add eager loading test using LEFT OUTER JOIN
|
|
||||||
end
|
end
|
||||||
|
|
||||||
# has_many through
|
# has_many through
|
||||||
@ -325,7 +323,7 @@ def test_distinct_has_many_through_a_has_many_through_association_on_through_ref
|
|||||||
|
|
||||||
def test_nested_has_many_through_with_a_table_referenced_multiple_times
|
def test_nested_has_many_through_with_a_table_referenced_multiple_times
|
||||||
author = authors(:bob)
|
author = authors(:bob)
|
||||||
assert_equal [posts(:misc_by_bob), posts(:misc_by_mary)], author.similar_posts.sort_by(&:id)
|
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.
|
# 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)
|
authors = Author.joins(:similar_posts).where('posts.id' => posts(:misc_by_bob).id)
|
||||||
@ -406,6 +404,42 @@ def test_nested_has_one_through_writers_should_raise_error
|
|||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
|
def test_nested_has_many_through_with_conditions_on_through_associations
|
||||||
|
blue, bob = tags(:blue), authors(:bob)
|
||||||
|
|
||||||
|
assert_equal [blue], bob.misc_post_first_blue_tags
|
||||||
|
|
||||||
|
# Pointless condition to force single-query loading
|
||||||
|
assert_includes_and_joins_equal(
|
||||||
|
Author.where('tags.id = tags.id'),
|
||||||
|
[bob], :misc_post_first_blue_tags
|
||||||
|
)
|
||||||
|
|
||||||
|
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 }
|
||||||
|
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_source_associations
|
||||||
|
blue, bob = tags(:blue), authors(:bob)
|
||||||
|
|
||||||
|
assert_equal [blue], bob.misc_post_first_blue_tags_2
|
||||||
|
|
||||||
|
# Pointless condition to force single-query loading
|
||||||
|
assert_includes_and_joins_equal(
|
||||||
|
Author.where('tags.id = tags.id'),
|
||||||
|
[bob], :misc_post_first_blue_tags_2
|
||||||
|
)
|
||||||
|
|
||||||
|
authors = assert_queries(4) { Author.includes(:misc_post_first_blue_tags_2).to_a }
|
||||||
|
assert_no_queries do
|
||||||
|
assert_equal [blue], authors[2].misc_post_first_blue_tags_2
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
private
|
private
|
||||||
|
|
||||||
def assert_includes_and_joins_equal(query, expected, association)
|
def assert_includes_and_joins_equal(query, expected, association)
|
||||||
|
@ -24,7 +24,7 @@ def test_each_should_raise_if_select_is_set_without_id
|
|||||||
end
|
end
|
||||||
|
|
||||||
def test_each_should_execute_if_id_is_in_select
|
def test_each_should_execute_if_id_is_in_select
|
||||||
assert_queries(5) do
|
assert_queries(6) do
|
||||||
Post.find_each(:select => "id, title, type", :batch_size => 2) do |post|
|
Post.find_each(:select => "id, title, type", :batch_size => 2) do |post|
|
||||||
assert_kind_of Post, post
|
assert_kind_of Post, post
|
||||||
end
|
end
|
||||||
|
@ -123,11 +123,13 @@ def test_find_all_with_prepared_limit_and_offset
|
|||||||
def test_find_all_with_limit_and_offset_and_multiple_order_clauses
|
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
|
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
|
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 [[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 [[1,4],[1,5],[1,6]], second_three_posts.map { |p| [p.author_id, p.id] }
|
||||||
assert_equal [[2,7],[2,9],[3,8]], 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
|
end
|
||||||
|
|
||||||
|
|
||||||
|
@ -501,22 +501,22 @@ def test_relation_merging_with_preload
|
|||||||
def test_count
|
def test_count
|
||||||
posts = Post.scoped
|
posts = Post.scoped
|
||||||
|
|
||||||
assert_equal 9, posts.count
|
assert_equal 11, posts.count
|
||||||
assert_equal 9, posts.count(:all)
|
assert_equal 11, posts.count(:all)
|
||||||
assert_equal 9, posts.count(:id)
|
assert_equal 11, posts.count(:id)
|
||||||
|
|
||||||
assert_equal 1, posts.where('comments_count > 1').count
|
assert_equal 1, posts.where('comments_count > 1').count
|
||||||
assert_equal 7, posts.where(:comments_count => 0).count
|
assert_equal 9, posts.where(:comments_count => 0).count
|
||||||
end
|
end
|
||||||
|
|
||||||
def test_count_with_distinct
|
def test_count_with_distinct
|
||||||
posts = Post.scoped
|
posts = Post.scoped
|
||||||
|
|
||||||
assert_equal 3, posts.count(:comments_count, :distinct => true)
|
assert_equal 3, posts.count(:comments_count, :distinct => true)
|
||||||
assert_equal 9, 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 3, posts.select(:comments_count).count(:distinct => true)
|
||||||
assert_equal 9, posts.select(:comments_count).count(:distinct => false)
|
assert_equal 11, posts.select(:comments_count).count(:distinct => false)
|
||||||
end
|
end
|
||||||
|
|
||||||
def test_count_explicit_columns
|
def test_count_explicit_columns
|
||||||
@ -526,7 +526,7 @@ def test_count_explicit_columns
|
|||||||
assert_equal [0], posts.select('comments_count').where('id is not null').group('id').order('id').count.values.uniq
|
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 0, posts.where('id is not null').select('comments_count').count
|
||||||
|
|
||||||
assert_equal 9, 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.select('comments_count').count
|
||||||
assert_equal 0, posts.count(:comments_count)
|
assert_equal 0, posts.count(:comments_count)
|
||||||
assert_equal 0, posts.count('comments_count')
|
assert_equal 0, posts.count('comments_count')
|
||||||
@ -541,12 +541,12 @@ def test_multiple_selects
|
|||||||
def test_size
|
def test_size
|
||||||
posts = Post.scoped
|
posts = Post.scoped
|
||||||
|
|
||||||
assert_queries(1) { assert_equal 9, posts.size }
|
assert_queries(1) { assert_equal 11, posts.size }
|
||||||
assert ! posts.loaded?
|
assert ! posts.loaded?
|
||||||
|
|
||||||
best_posts = posts.where(:comments_count => 0)
|
best_posts = posts.where(:comments_count => 0)
|
||||||
best_posts.to_a # force load
|
best_posts.to_a # force load
|
||||||
assert_no_queries { assert_equal 7, best_posts.size }
|
assert_no_queries { assert_equal 9, best_posts.size }
|
||||||
end
|
end
|
||||||
|
|
||||||
def test_count_complex_chained_relations
|
def test_count_complex_chained_relations
|
||||||
|
14
activerecord/test/fixtures/posts.yml
vendored
14
activerecord/test/fixtures/posts.yml
vendored
@ -64,3 +64,17 @@ misc_by_mary:
|
|||||||
title: misc post by mary
|
title: misc post by mary
|
||||||
body: hello
|
body: hello
|
||||||
type: Post
|
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
|
||||||
|
28
activerecord/test/fixtures/taggings.yml
vendored
28
activerecord/test/fixtures/taggings.yml
vendored
@ -38,3 +38,31 @@ misc_post_by_mary:
|
|||||||
tag_id: 2
|
tag_id: 2
|
||||||
taggable_id: 9
|
taggable_id: 9
|
||||||
taggable_type: Post
|
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
|
||||||
|
4
activerecord/test/fixtures/tags.yml
vendored
4
activerecord/test/fixtures/tags.yml
vendored
@ -5,3 +5,7 @@ general:
|
|||||||
misc:
|
misc:
|
||||||
id: 2
|
id: 2
|
||||||
name: Misc
|
name: Misc
|
||||||
|
|
||||||
|
blue:
|
||||||
|
id: 3
|
||||||
|
name: Blue
|
||||||
|
@ -86,7 +86,7 @@ def testing_proxy_target
|
|||||||
has_many :tagging, :through => :posts
|
has_many :tagging, :through => :posts
|
||||||
has_many :taggings, :through => :posts
|
has_many :taggings, :through => :posts
|
||||||
has_many :tags, :through => :posts
|
has_many :tags, :through => :posts
|
||||||
has_many :similar_posts, :through => :tags, :source => :tagged_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 :distinct_tags, :through => :posts, :source => :tags, :select => "DISTINCT tags.*", :order => "tags.name"
|
||||||
has_many :post_categories, :through => :posts, :source => :categories
|
has_many :post_categories, :through => :posts, :source => :categories
|
||||||
has_many :tagging_tags, :through => :taggings, :source => :tag
|
has_many :tagging_tags, :through => :taggings, :source => :tag
|
||||||
@ -103,6 +103,11 @@ def testing_proxy_target
|
|||||||
|
|
||||||
has_many :post_categories, :through => :posts, :source => :categories
|
has_many :post_categories, :through => :posts, :source => :categories
|
||||||
has_many :category_post_comments, :through => :categories, :source => :post_comments
|
has_many :category_post_comments, :through => :categories, :source => :post_comments
|
||||||
|
|
||||||
|
has_many :misc_posts, :class_name => 'Post', :conditions => "posts.title LIKE 'misc post%'"
|
||||||
|
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 LIKE 'misc post%'"
|
||||||
|
|
||||||
scope :relation_include_posts, includes(:posts)
|
scope :relation_include_posts, includes(:posts)
|
||||||
scope :relation_include_tags, includes(:tags)
|
scope :relation_include_tags, includes(:tags)
|
||||||
|
@ -64,6 +64,11 @@ def add_joins_and_select
|
|||||||
has_many :funky_tags, :through => :taggings, :source => :tag
|
has_many :funky_tags, :through => :taggings, :source => :tag
|
||||||
has_many :super_tags, :through => :taggings
|
has_many :super_tags, :through => :taggings
|
||||||
has_one :tagging, :as => :taggable
|
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_taggings, :as => :taggable, :class_name => "Tagging", :conditions => 'taggings.id < 0'
|
||||||
has_many :invalid_tags, :through => :invalid_taggings, :source => :tag
|
has_many :invalid_tags, :through => :invalid_taggings, :source => :tag
|
||||||
|
@ -6,5 +6,6 @@ class Tagging < ActiveRecord::Base
|
|||||||
belongs_to :tag, :include => :tagging
|
belongs_to :tag, :include => :tagging
|
||||||
belongs_to :super_tag, :class_name => 'Tag', :foreign_key => 'super_tag_id'
|
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 :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 :taggable, :polymorphic => true, :counter_cache => true
|
belongs_to :taggable, :polymorphic => true, :counter_cache => true
|
||||||
end
|
end
|
||||||
|
@ -537,6 +537,7 @@ def create_table(*args, &block)
|
|||||||
t.column :super_tag_id, :integer
|
t.column :super_tag_id, :integer
|
||||||
t.column :taggable_type, :string
|
t.column :taggable_type, :string
|
||||||
t.column :taggable_id, :integer
|
t.column :taggable_id, :integer
|
||||||
|
t.string :comment
|
||||||
end
|
end
|
||||||
|
|
||||||
create_table :tags, :force => true do |t|
|
create_table :tags, :force => true do |t|
|
||||||
|
Loading…
Reference in New Issue
Block a user