Skip to content
This repository
Browse code

Introduce finder :joins with associations. Same :include syntax but w…

…ith inner rather than outer joins. Closes #10012.

git-svn-id: http://svn-commit.rubyonrails.org/rails/trunk@8054 5ecf4fe2-1ee6-0310-87b1-e25e094e27de
  • Loading branch information...
commit bef071dd0b6b634c5e8e49d8c1b9daa0baa4136c 1 parent 2283524
Jeremy Kemper authored October 29, 2007
7  activerecord/CHANGELOG
... ...
@@ -1,5 +1,12 @@
1 1
 *SVN*
2 2
 
  3
+* Introduce finder :joins with associations. Same :include syntax but with inner rather than outer joins.  #10012 [RubyRedRick]
  4
+    # Find users with an avatar
  5
+    User.find(:all, :joins => :avatar)
  6
+
  7
+    # Find posts with a high-rated comment.
  8
+    Post.find(:all, :joins => :comments, :conditions => 'comments.rating > 3')
  9
+
3 10
 * Associations: speedup duplicate record check.  #10011 [lifofifo]
4 11
 
5 12
 * Make sure that << works on has_many associations on unsaved records.  Closes #9989 [hasmanyjosh]
103  activerecord/lib/active_record/associations.rb
@@ -486,7 +486,63 @@ def clear_association_cache #:nodoc:
486 486
     # 
487 487
     # When eager loaded, conditions are interpolated in the context of the model class, not the model instance.  Conditions are lazily interpolated
488 488
     # before the actual model exists.
489  
-    # 
  489
+    #
  490
+    # == Adding Joins For Associations to Queries Using the :joins option
  491
+    #
  492
+    # ActiveRecord::Base#find provides a :joins option, which takes either a string or values accepted by the :include option.
  493
+    # if the value is a string, the it should contain a SQL fragment containing a join clause.
  494
+    #
  495
+    # Non-string values of :joins will add an automatic join clause to the query in the same way that the :include option does but with two critical
  496
+    # differences:
  497
+    #
  498
+    #     1. A normal (inner) join will be performed instead of the outer join generated by :include.
  499
+    #        this means that only objects which have objects attached to the association will be included in the result.
  500
+    #        For example, suppose we have the following tables (in yaml format):
  501
+    #
  502
+    #        Authors
  503
+    #          fred:
  504
+    #            id: 1
  505
+    #            name: Fred
  506
+    #          steve:
  507
+    #            id: 2
  508
+    #            name: Steve
  509
+    #
  510
+    #        Contributions
  511
+    #          only:
  512
+    #            id: 1
  513
+    #            author_id: 1
  514
+    #            description: Atta Boy Letter for Steve
  515
+    #            date: 2007-10-27 14:09:54
  516
+    #
  517
+    #        and corresponding AR Classes
  518
+    #
  519
+    #        class Author: < ActiveRecord::Base
  520
+    #            has_many :contributions
  521
+    #        end
  522
+    #
  523
+    #        class Contribution < ActiveRecord::Base
  524
+    #            belongs_to :author
  525
+    #        end
  526
+    #
  527
+    #        The query Author.find(:all) will return both authors, but Author.find(:all, :joins => :contributions) will
  528
+    #        only return authors who have at least one contribution, in this case only the first.
  529
+    #        This is only a degenerate case of the more typical use of :joins with a non-string value.
  530
+    #        For example to find authors who have at least one contribution before a certain date we can use:
  531
+    #
  532
+    #            Author.find(:all, :joins => :contributions, :conditions => ["contributions.date <= ?", 1.week.ago.to_s(:db)])
  533
+    #
  534
+    #     2. Only instances of the class to which the find is sent will be instantiated. ActiveRecord objects will not
  535
+    #        be instantiated for rows reached through the associations.
  536
+    #
  537
+    #  The difference between using :joins vs :include to name associated records is that :joins allows associated tables to
  538
+    #  participate in selection criteria in the query without incurring the overhead of instantiating associated objects.
  539
+    #  This can be important when the number of associated objects in the database is large, and they will not be used, or
  540
+    #  only those associated with a paricular object or objects will be used after the query, making two queries more
  541
+    #  efficient than one.
  542
+    #
  543
+    #  Note that while using a string value for :joins marks the result objects as read-only, the objects resulting
  544
+    #  from a call to find with a non-string :joins option value will be writable.
  545
+    #
490 546
     # == Table Aliasing
491 547
     #
492 548
     # ActiveRecord uses table aliasing in the case that a table is referenced multiple times in a join.  If a table is referenced only once,
@@ -1121,7 +1177,13 @@ def association_constructor_method(constructor, reflection, association_proxy_cl
1121 1177
         
1122 1178
         def find_with_associations(options = {})
1123 1179
           catch :invalid_query do
1124  
-            join_dependency = JoinDependency.new(self, merge_includes(scope(:find, :include), options[:include]), options[:joins])
  1180
+            if ar_joins = scope(:find, :ar_joins)
  1181
+              options = options.dup
  1182
+              options[:ar_joins] = ar_joins
  1183
+            end
  1184
+            includes = merge_includes(scope(:find, :include), options[:include])
  1185
+            includes = merge_includes(includes, options[:ar_joins])
  1186
+            join_dependency = JoinDependency.new(self, includes, options[:joins], options[:ar_joins])
1125 1187
             rows = select_all_rows(options, join_dependency)
1126 1188
             return join_dependency.instantiate(rows)
1127 1189
           end
@@ -1375,8 +1437,9 @@ def create_extension_modules(association_id, block_extension, extensions)
1375 1437
         class JoinDependency # :nodoc:
1376 1438
           attr_reader :joins, :reflections, :table_aliases
1377 1439
 
1378  
-          def initialize(base, associations, joins)
  1440
+          def initialize(base, associations, joins, ar_joins = nil)
1379 1441
             @joins                 = [JoinBase.new(base, joins)]
  1442
+            @ar_joins              = ar_joins
1380 1443
             @associations          = associations
1381 1444
             @reflections           = []
1382 1445
             @base_records_hash     = {}
@@ -1400,9 +1463,9 @@ def instantiate(rows)
1400 1463
               unless @base_records_hash[primary_id]
1401 1464
                 @base_records_in_order << (@base_records_hash[primary_id] = join_base.instantiate(row))
1402 1465
               end
1403  
-              construct(@base_records_hash[primary_id], @associations, join_associations.dup, row)
  1466
+              construct(@base_records_hash[primary_id], @associations, join_associations.dup, row) unless @ar_joins
1404 1467
             end
1405  
-            remove_duplicate_results!(join_base.active_record, @base_records_in_order, @associations)
  1468
+            remove_duplicate_results!(join_base.active_record, @base_records_in_order, @associations) unless @ar_joins
1406 1469
             return @base_records_in_order
1407 1470
           end
1408 1471
 
@@ -1444,7 +1507,7 @@ def build(associations, parent = nil)
1444 1507
                   reflection = parent.reflections[associations.to_s.intern] or
1445 1508
                   raise ConfigurationError, "Association named '#{ associations }' was not found; perhaps you misspelled it?"
1446 1509
                   @reflections << reflection
1447  
-                  @joins << JoinAssociation.new(reflection, self, parent)
  1510
+                  @joins << (@ar_joins ? ARJoinAssociation : JoinAssociation).new(reflection, self, parent)
1448 1511
                 when Array
1449 1512
                   associations.each do |association|
1450 1513
                     build(association, parent)
@@ -1595,12 +1658,12 @@ def initialize(reflection, join_dependency, parent = nil)
1595 1658
             def association_join
1596 1659
               join = case reflection.macro
1597 1660
                 when :has_and_belongs_to_many
1598  
-                  " LEFT OUTER JOIN %s ON %s.%s = %s.%s " % [
  1661
+                  " #{join_type} %s ON %s.%s = %s.%s " % [
1599 1662
                      table_alias_for(options[:join_table], aliased_join_table_name),
1600 1663
                      aliased_join_table_name,
1601 1664
                      options[:foreign_key] || reflection.active_record.to_s.foreign_key,
1602 1665
                      parent.aliased_table_name, reflection.active_record.primary_key] +
1603  
-                  " LEFT OUTER JOIN %s ON %s.%s = %s.%s " % [
  1666
+                  " #{join_type} %s ON %s.%s = %s.%s " % [
1604 1667
                      table_name_and_alias, aliased_table_name, klass.primary_key,
1605 1668
                      aliased_join_table_name, options[:association_foreign_key] || klass.to_s.foreign_key
1606 1669
                      ]
@@ -1658,13 +1721,13 @@ def association_join
1658 1721
                         end
1659 1722
                       end
1660 1723
 
1661  
-                      " LEFT OUTER JOIN %s ON (%s.%s = %s.%s%s%s%s) " % [
  1724
+                      " #{join_type} %s ON (%s.%s = %s.%s%s%s%s) " % [
1662 1725
                         table_alias_for(through_reflection.klass.table_name, aliased_join_table_name),
1663 1726
                         parent.aliased_table_name, reflection.active_record.connection.quote_column_name(parent.primary_key),
1664 1727
                         aliased_join_table_name, reflection.active_record.connection.quote_column_name(jt_foreign_key), 
1665 1728
                         jt_as_extra, jt_source_extra, jt_sti_extra
1666 1729
                       ] +
1667  
-                      " LEFT OUTER JOIN %s ON (%s.%s = %s.%s%s) " % [
  1730
+                      " #{join_type} %s ON (%s.%s = %s.%s%s) " % [
1668 1731
                         table_name_and_alias, 
1669 1732
                         aliased_table_name, reflection.active_record.connection.quote_column_name(first_key),
1670 1733
                         aliased_join_table_name, reflection.active_record.connection.quote_column_name(second_key),
@@ -1672,7 +1735,7 @@ def association_join
1672 1735
                       ]
1673 1736
 
1674 1737
                     when reflection.options[:as] && [:has_many, :has_one].include?(reflection.macro)
1675  
-                      " LEFT OUTER JOIN %s ON %s.%s = %s.%s AND %s.%s = %s" % [
  1738
+                      " #{join_type} %s ON %s.%s = %s.%s AND %s.%s = %s" % [
1676 1739
                         table_name_and_alias,
1677 1740
                         aliased_table_name, "#{reflection.options[:as]}_id",
1678 1741
                         parent.aliased_table_name, parent.primary_key,
@@ -1681,14 +1744,14 @@ def association_join
1681 1744
                       ]
1682 1745
                     else
1683 1746
                       foreign_key = options[:foreign_key] || reflection.active_record.name.foreign_key
1684  
-                      " LEFT OUTER JOIN %s ON %s.%s = %s.%s " % [
  1747
+                      " #{join_type} %s ON %s.%s = %s.%s " % [
1685 1748
                         table_name_and_alias,
1686 1749
                         aliased_table_name, foreign_key,
1687 1750
                         parent.aliased_table_name, parent.primary_key
1688 1751
                       ]
1689 1752
                   end
1690 1753
                 when :belongs_to
1691  
-                  " LEFT OUTER JOIN %s ON %s.%s = %s.%s " % [
  1754
+                  " #{join_type} %s ON %s.%s = %s.%s " % [
1692 1755
                      table_name_and_alias, aliased_table_name, reflection.klass.primary_key,
1693 1756
                      parent.aliased_table_name, options[:foreign_key] || klass.to_s.foreign_key
1694 1757
                     ]
@@ -1723,7 +1786,19 @@ def table_name_and_alias
1723 1786
 
1724 1787
               def interpolate_sql(sql)
1725 1788
                 instance_eval("%@#{sql.gsub('@', '\@')}@") 
1726  
-              end 
  1789
+              end
  1790
+
  1791
+           private
  1792
+              def join_type
  1793
+                "LEFT OUTER JOIN"
  1794
+              end
  1795
+
  1796
+          end
  1797
+          class ARJoinAssociation < JoinAssociation
  1798
+            private
  1799
+              def join_type
  1800
+                "INNER JOIN"
  1801
+              end
1727 1802
           end
1728 1803
         end
1729 1804
     end
34  activerecord/lib/active_record/base.rb
@@ -380,9 +380,11 @@ class << self # Class methods
380 380
       # * <tt>:group</tt>: An attribute name by which the result should be grouped. Uses the GROUP BY SQL-clause.
381 381
       # * <tt>:limit</tt>: An integer determining the limit on the number of rows that should be returned.
382 382
       # * <tt>:offset</tt>: An integer determining the offset from where the rows should be fetched. So at 5, it would skip rows 0 through 4.
383  
-      # * <tt>:joins</tt>: An SQL fragment for additional joins like "LEFT JOIN comments ON comments.post_id = id". (Rarely needed).
384  
-      #   The records will be returned read-only since they will have attributes that do not correspond to the table's columns.
  383
+      # * <tt>:joins</tt>: Either an SQL fragment for additional joins like "LEFT JOIN comments ON comments.post_id = id". (Rarely needed).
  384
+      #    or names associations in the same form used for the :include option.
  385
+      #   If the value is a string, then the records will be returned read-only since they will have attributes that do not correspond to the table's columns.
385 386
       #   Pass :readonly => false to override.
  387
+      #   See adding joins for associations under Association.
386 388
       # * <tt>:include</tt>: Names associations that should be loaded alongside using LEFT OUTER JOINs. The symbols named refer
387 389
       #   to already defined associations. See eager loading under Associations.
388 390
       # * <tt>:select</tt>: By default, this is * as in SELECT * FROM, but can be changed if you for example want to do a join, but not
@@ -428,8 +430,17 @@ class << self # Class methods
428 430
       #   end
429 431
       def find(*args)
430 432
         options = args.extract_options!
  433
+        # Note:  we extract any :joins option with a non-string value from the options, and turn it into
  434
+        #  an internal option :ar_joins.  This allows code called from her to find the ar_joins, and
  435
+        #  it bypasses marking the result as read_only.
  436
+        #  A normal string join marks the result as read-only because it contains attributes from joined tables
  437
+        #  which are not in the base table and therefore prevent the result from being saved.
  438
+        #  In the case of an ar_join, the JoinDependency created to instantiate the results eliminates these
  439
+        #  bogus attributes.  See JoinDependency#instantiate, and JoinBase#instantiate in associations.rb.
  440
+        options, ar_joins = *extract_ar_join_from_options(options)
431 441
         validate_find_options(options)
432 442
         set_readonly_option!(options)
  443
+        options[:ar_joins] = ar_joins if ar_joins
433 444
 
434 445
         case args.first
435 446
           when :first then find_initial(options)
@@ -1020,8 +1031,17 @@ def find_initial(options)
1020 1031
           find_every(options).first
1021 1032
         end
1022 1033
 
  1034
+        # If options contains :joins, with a non-string value
  1035
+        #  remove it from options
  1036
+        # return the updated or unchanged options, and the ar_join value or nil
  1037
+        def extract_ar_join_from_options(options)
  1038
+          new_options = options.dup
  1039
+          join_option = new_options.delete(:joins)
  1040
+          (join_option && !join_option.kind_of?(String)) ? [new_options, join_option] : [options, nil]
  1041
+        end
  1042
+
1023 1043
         def find_every(options)
1024  
-          records = scoped?(:find, :include) || options[:include] ?
  1044
+          records = scoped?(:find, :include) || options[:include] || scoped?(:find, :ar_joins) || (options[:ar_joins]) ?
1025 1045
             find_with_associations(options) : 
1026 1046
             find_by_sql(construct_finder_sql(options))
1027 1047
 
@@ -1445,7 +1465,13 @@ def with_scope(method_scoping = {}, action = :merge, &block)
1445 1465
 
1446 1466
           if f = method_scoping[:find]
1447 1467
             f.assert_valid_keys(VALID_FIND_OPTIONS)
  1468
+            # see note about :joins and :ar_joins in ActiveRecord::Base#find
  1469
+            f, ar_joins = *extract_ar_join_from_options(f)
1448 1470
             set_readonly_option! f
  1471
+            if ar_joins
  1472
+              f[:ar_joins] = ar_joins
  1473
+              method_scoping[:find] = f
  1474
+            end
1449 1475
           end
1450 1476
 
1451 1477
           # Merge scopings
@@ -1458,7 +1484,7 @@ def with_scope(method_scoping = {}, action = :merge, &block)
1458 1484
                       merge = hash[method][key] && params[key] # merge if both scopes have the same key
1459 1485
                       if key == :conditions && merge
1460 1486
                         hash[method][key] = [params[key], hash[method][key]].collect{ |sql| "( %s )" % sanitize_sql(sql) }.join(" AND ")
1461  
-                      elsif key == :include && merge
  1487
+                      elsif ([:include, :ar_joins].include?(key)) && merge
1462 1488
                         hash[method][key] = merge_includes(hash[method][key], params[key]).uniq
1463 1489
                       else
1464 1490
                         hash[method][key] = hash[method][key] || params[key]
19  activerecord/lib/active_record/calculations.rb
@@ -15,8 +15,11 @@ module ClassMethods
15 15
       # The third approach, count using options, accepts an option hash as the only parameter. The options are:
16 16
       #
17 17
       # * <tt>:conditions</tt>: An SQL fragment like "administrator = 1" or [ "user_name = ?", username ]. See conditions in the intro.
18  
-      # * <tt>:joins</tt>: An SQL fragment for additional joins like "LEFT JOIN comments ON comments.post_id = id". (Rarely needed).
19  
-      #   The records will be returned read-only since they will have attributes that do not correspond to the table's columns.
  18
+      # * <tt>:joins</tt>: Either an SQL fragment for additional joins like "LEFT JOIN comments ON comments.post_id = id". (Rarely needed).
  19
+      #    or names associations in the same form used for the :include option.
  20
+      #   If the value is a string, then the records will be returned read-only since they will have attributes that do not correspond to the table's columns.
  21
+      #   Pass :readonly => false to override.
  22
+      #   See adding joins for associations under Association.
20 23
       # * <tt>:include</tt>: Named associations that should be loaded alongside using LEFT OUTER JOINs. The symbols named refer
21 24
       #   to already defined associations. When using named associations count returns the number DISTINCT items for the model you're counting.
22 25
       #   See eager loading under Associations.
@@ -109,7 +112,9 @@ def sum(column_name, options = {})
109 112
       #   Person.minimum(:age, :conditions => ['last_name != ?', 'Drake']) # Selects the minimum age for everyone with a last name other than 'Drake'
110 113
       #   Person.minimum(:age, :having => 'min(age) > 17', :group => :last_name) # Selects the minimum age for any family without any minors
111 114
       def calculate(operation, column_name, options = {})
  115
+        options, ar_joins = *extract_ar_join_from_options(options)
112 116
         validate_calculation_options(operation, options)
  117
+        options[:ar_joins] = ar_joins if ar_joins
113 118
         column_name     = options[:select] if options[:select]
114 119
         column_name     = '*' if column_name == :all
115 120
         column          = column_for column_name
@@ -149,8 +154,14 @@ def construct_calculation_sql(operation, column_name, options) #:nodoc:
149 154
           operation = operation.to_s.downcase
150 155
           options = options.symbolize_keys
151 156
 
152  
-          scope           = scope(:find)
  157
+          scope = scope(:find)
  158
+          if scope && scope[:ar_joins]
  159
+            scope = scope.dup
  160
+            options = options.dup
  161
+            options[:ar_joins] = scope.delete(:ar_joins)
  162
+          end
153 163
           merged_includes = merge_includes(scope ? scope[:include] : [], options[:include])
  164
+          merged_includes = merge_includes(merged_includes, options[:ar_joins])
154 165
           aggregate_alias = column_alias_for(operation, column_name)
155 166
 
156 167
           if operation == 'count'
@@ -173,7 +184,7 @@ def construct_calculation_sql(operation, column_name, options) #:nodoc:
173 184
           sql << " FROM (SELECT DISTINCT #{column_name}" if use_workaround
174 185
           sql << " FROM #{table_name} "
175 186
           if merged_includes.any?
176  
-            join_dependency = ActiveRecord::Associations::ClassMethods::JoinDependency.new(self, merged_includes, options[:joins])
  187
+            join_dependency = ActiveRecord::Associations::ClassMethods::JoinDependency.new(self, merged_includes, options[:joins], options[:ar_joins])
177 188
             sql << join_dependency.join_associations.collect{|join| join.association_join }.join
178 189
           end
179 190
           add_joins!(sql, options, scope)
158  activerecord/test/associations/ar_joins_test.rb
... ...
@@ -0,0 +1,158 @@
  1
+require 'abstract_unit'
  2
+require 'fixtures/post'
  3
+require 'fixtures/comment'
  4
+require 'fixtures/author'
  5
+require 'fixtures/category'
  6
+require 'fixtures/categorization'
  7
+require 'fixtures/company'
  8
+require 'fixtures/topic'
  9
+require 'fixtures/reply'
  10
+require 'fixtures/developer'
  11
+require 'fixtures/project'
  12
+
  13
+class ArJoinsTest < Test::Unit::TestCase
  14
+  fixtures :authors, :posts, :comments, :categories, :categories_posts, :people,
  15
+           :developers, :projects, :developers_projects,
  16
+           :categorizations, :companies, :accounts, :topics
  17
+
  18
+  def test_ar_joins
  19
+    authors = Author.find(:all, :joins => :posts, :conditions => ['posts.type = ?', "Post"])
  20
+    assert_not_equal(0 , authors.length)
  21
+    authors.each do |author|
  22
+      assert !(author.send(:instance_variables).include? "@posts")
  23
+      assert(!author.readonly?, "non-string join value produced read-only result.")
  24
+    end
  25
+  end
  26
+
  27
+  def test_ar_joins_with_cascaded_two_levels
  28
+    authors = Author.find(:all, :joins=>{:posts=>:comments})
  29
+    assert_equal(2, authors.length)
  30
+    authors.each do |author|
  31
+      assert !(author.send(:instance_variables).include? "@posts")
  32
+      assert(!author.readonly?, "non-string join value produced read-only result.")
  33
+    end
  34
+    authors = Author.find(:all, :joins=>{:posts=>:comments}, :conditions => ["comments.body = ?", "go crazy" ])
  35
+    assert_equal(1, authors.length)
  36
+    authors.each do |author|
  37
+      assert !(author.send(:instance_variables).include? "@posts")
  38
+      assert(!author.readonly?, "non-string join value produced read-only result.")
  39
+    end
  40
+  end
  41
+
  42
+
  43
+  def test_ar_joins_with_complex_conditions
  44
+    authors = Author.find(:all, :joins=>{:posts=>[:comments, :categories]},
  45
+    :conditions => ["categories.name = ?  AND posts.title = ?", "General", "So I was thinking"]
  46
+    )
  47
+    assert_equal(1, authors.length)
  48
+    authors.each do |author|
  49
+      assert !(author.send(:instance_variables).include? "@posts")
  50
+      assert(!author.readonly?, "non-string join value produced read-only result.")
  51
+    end
  52
+    assert_equal("David", authors.first.name)
  53
+  end
  54
+
  55
+  def test_ar_join_with_has_many_and_limit_and_scoped_and_explicit_conditions
  56
+    Post.with_scope(:find => { :conditions => "1=1" }) do
  57
+      posts = authors(:david).posts.find(:all,
  58
+        :joins    => :comments,
  59
+        :conditions => "comments.body like 'Normal%' OR comments.#{QUOTED_TYPE}= 'SpecialComment'",
  60
+        :limit      => 2
  61
+      )
  62
+      assert_equal 2, posts.size
  63
+
  64
+      count = Post.count(
  65
+        :joins    => [ :comments, :author ],
  66
+        :conditions => "authors.name = 'David' AND (comments.body like 'Normal%' OR comments.#{QUOTED_TYPE}= 'SpecialComment')",
  67
+        :limit      => 2
  68
+      )
  69
+      assert_equal count, posts.size
  70
+    end
  71
+  end
  72
+
  73
+  def test_ar_join_with_scoped_order_using_association_limiting_without_explicit_scope
  74
+    posts_with_explicit_order = Post.find(:all, :conditions => 'comments.id is not null', :joins => :comments, :order => 'posts.id DESC', :limit => 2)
  75
+    posts_with_scoped_order = Post.with_scope(:find => {:order => 'posts.id DESC'}) do
  76
+      Post.find(:all, :conditions => 'comments.id is not null', :joins => :comments, :limit => 2)
  77
+    end
  78
+    assert_equal posts_with_explicit_order, posts_with_scoped_order
  79
+  end
  80
+
  81
+  def test_scoped_find_include
  82
+    # with the include, will retrieve only developers for the given project
  83
+    scoped_developers = Developer.with_scope(:find => { :joins => :projects }) do
  84
+      Developer.find(:all, :conditions => 'projects.id = 2')
  85
+    end
  86
+    assert scoped_developers.include?(developers(:david))
  87
+    assert !scoped_developers.include?(developers(:jamis))
  88
+    assert_equal 1, scoped_developers.size
  89
+  end
  90
+
  91
+
  92
+  def test_nested_scoped_find_ar_join
  93
+    Developer.with_scope(:find => { :joins => :projects }) do
  94
+      Developer.with_scope(:find => { :conditions => "projects.id = 2" }) do
  95
+        assert_equal('David', Developer.find(:first).name)
  96
+      end
  97
+    end
  98
+  end
  99
+
  100
+  def test_nested_scoped_find_merged_ar_join
  101
+    # :include's remain unique and don't "double up" when merging
  102
+    Developer.with_scope(:find => { :joins => :projects, :conditions => "projects.id = 2" }) do
  103
+      Developer.with_scope(:find => { :joins => :projects }) do
  104
+        assert_equal 1, Developer.instance_eval('current_scoped_methods')[:find][:ar_joins].length
  105
+        assert_equal('David', Developer.find(:first).name)
  106
+      end
  107
+    end
  108
+    # the nested scope doesn't remove the first :include
  109
+    Developer.with_scope(:find => { :joins => :projects, :conditions => "projects.id = 2" }) do
  110
+      Developer.with_scope(:find => { :joins => [] }) do
  111
+        assert_equal 1, Developer.instance_eval('current_scoped_methods')[:find][:ar_joins].length
  112
+        assert_equal('David', Developer.find(:first).name)
  113
+      end
  114
+    end
  115
+    # mixing array and symbol include's will merge correctly
  116
+    Developer.with_scope(:find => { :joins => [:projects], :conditions => "projects.id = 2" }) do
  117
+      Developer.with_scope(:find => { :joins => :projects }) do
  118
+        assert_equal 1, Developer.instance_eval('current_scoped_methods')[:find][:ar_joins].length
  119
+        assert_equal('David', Developer.find(:first).name)
  120
+      end
  121
+    end
  122
+  end
  123
+
  124
+  def test_nested_scoped_find_replace_include
  125
+    Developer.with_scope(:find => { :joins => :projects }) do
  126
+      Developer.with_exclusive_scope(:find => { :joins => [] }) do
  127
+        assert_equal 0, Developer.instance_eval('current_scoped_methods')[:find][:ar_joins].length
  128
+      end
  129
+    end
  130
+  end
  131
+
  132
+#
  133
+# Calculations
  134
+#
  135
+  def test_count_with_ar_joins
  136
+    assert_equal(2, Author.count(:joins => :posts, :conditions => ['posts.type = ?', "Post"]))
  137
+    assert_equal(1, Author.count(:joins => :posts, :conditions => ['posts.type = ?', "SpecialPost"]))
  138
+  end
  139
+
  140
+  def test_should_get_maximum_of_field_with_joins
  141
+    assert_equal 50, Account.maximum(:credit_limit, :joins=> :firm, :conditions => "companies.name != 'Summit'")
  142
+  end
  143
+
  144
+  def test_should_get_maximum_of_field_with_scoped_include
  145
+    Account.with_scope :find => { :joins => :firm, :conditions => "companies.name != 'Summit'" } do
  146
+      assert_equal 50, Account.maximum(:credit_limit)
  147
+    end
  148
+  end
  149
+
  150
+  def test_should_not_modify_options_when_using_ar_joins_on_count
  151
+    options = {:conditions => 'companies.id > 1', :joins => :firm}
  152
+    options_copy = options.dup
  153
+
  154
+    Account.count(:all, options)
  155
+    assert_equal options_copy, options
  156
+  end
  157
+
  158
+end

0 notes on commit bef071d

Please sign in to comment.
Something went wrong with that request. Please try again.