Ruby on Rails | Screencasts | Download | Documentation | Weblog | Community | Source

Changeset 3553

Show
Ignore:
Timestamp:
02/09/06 09:17:40 (3 years ago)
Author:
nzkoz
Message:

* Fix pagination problems when using include
* Introduce Unit Tests for pagination
* Allow count to work with :include by using count distinct.

[Kevin Clark & Jeremy Hopple]

Files:

Legend:

Unmodified
Added
Removed
Modified
Copied
Moved
  • trunk/actionpack/CHANGELOG

    r3552 r3553  
    11*SVN* 
     2 
     3* Fix problems with pagination and :include.  [Kevin Clark] 
     4 
     5* Add ActiveRecordTestCase for testing AR integration. [Kevin Clark] 
     6 
     7* Add Unit Tests for pagination [Kevin Clark] 
    28 
    39* Add :html option for specifying form tag options in form_for. [Sam Stephenson] 
  • trunk/actionpack/lib/action_controller/pagination.rb

    r2878 r3553  
    164164    # the +model+ and given +conditions+. Override this method to implement a 
    165165    # custom counter. 
    166     def count_collection_for_pagination(model, conditions, joins) 
    167       model.count(conditions,joins) 
    168     end 
    169    
     166    def count_collection_for_pagination(model, options) 
     167      model.count(:conditions => options[:conditions], 
     168                  :joins => options[:join] || options[:joins], :include => options[:include]) 
     169    end 
     170     
    170171    # Returns a collection of items for the given +model+ and +options[conditions]+, 
    171172    # ordered by +options[order]+, for the current page in the given +paginator+. 
     
    186187      klass = options[:class_name].constantize 
    187188      page  = @params[options[:parameter]] 
    188       count = count_collection_for_pagination(klass, options[:conditions], 
    189                                               options[:join] || options[:joins]) 
    190  
     189      count = count_collection_for_pagination(klass, options) 
    191190      paginator = Paginator.new(self, count, options[:per_page], page) 
    192191      collection = find_collection_for_pagination(klass, options, paginator) 
    193        
     192     
    194193      return paginator, collection  
    195194    end 
  • trunk/actionpack/Rakefile

    r3543 r3553  
    3131  t.verbose = true 
    3232} 
     33 
     34desc 'ActiveRecord Integration Tests' 
     35Rake::TestTask.new(:test_active_record_integration) do |t| 
     36  t.libs << "test" 
     37  t.test_files = Dir.glob("test/activerecord/*_test.rb") 
     38  t.verbose = true 
     39end 
    3340 
    3441 
  • trunk/activerecord/CHANGELOG

    r3544 r3553  
    11*SVN* 
     2 
     3* Fix problems with count when used with :include [Jeremy Hopple and Kevin Clark] 
    24 
    35* ActiveRecord::RecordInvalid now states which validations failed in its default error message [Tobias Luetke] 
  • trunk/activerecord/lib/active_record/associations.rb

    r3331 r3553  
    780780          end 
    781781        end 
     782         
     783        def count_with_associations(options = {}) 
     784          reflections = reflect_on_included_associations(options[:include]) 
     785          return count_by_sql(construct_counter_sql_with_included_associations(options, reflections)) 
     786        end 
    782787 
    783788        def find_with_associations(options = {}) 
     
    9971002          ) 
    9981003        end 
     1004         
     1005        def construct_counter_sql_with_included_associations(options, reflections) 
     1006          sql = "SELECT COUNT(DISTINCT #{table_name}.#{primary_key})" 
     1007           
     1008          # A (slower) workaround if we're using a backend, like sqlite, that doesn't support COUNT DISTINCT. 
     1009          if !Base.connection.supports_count_distinct? 
     1010            sql = "SELECT COUNT(*) FROM (SELECT DISTINCT #{table_name}.#{primary_key}" 
     1011          end 
     1012           
     1013          sql << " FROM #{table_name} " 
     1014          sql << reflections.collect { |reflection| association_join(reflection) }.to_s 
     1015          sql << "#{options[:joins]} " if options[:joins] 
     1016 
     1017          add_conditions!(sql, options[:conditions]) 
     1018          add_sti_conditions!(sql, reflections) 
     1019          add_limited_ids_condition!(sql, options, reflections) if !using_limitable_reflections?(reflections) && options[:limit] 
     1020 
     1021          add_limit!(sql, options) if using_limitable_reflections?(reflections) 
     1022 
     1023          if !Base.connection.supports_count_distinct? 
     1024            sql << ")" 
     1025          end 
     1026 
     1027          return sanitize_sql(sql)           
     1028        end 
    9991029 
    10001030        def construct_finder_sql_with_included_associations(options, schema_abbreviations, reflections) 
     
    10021032          sql << reflections.collect { |reflection| association_join(reflection) }.to_s 
    10031033          sql << "#{options[:joins]} " if options[:joins] 
    1004  
     1034  
    10051035          add_conditions!(sql, options[:conditions]) 
    10061036          add_sti_conditions!(sql, reflections) 
    1007           add_limited_ids_condition!(sql, options) if !using_limitable_reflections?(reflections) && options[:limit] 
     1037          add_limited_ids_condition!(sql, options, reflections) if !using_limitable_reflections?(reflections) && options[:limit] 
    10081038 
    10091039          sql << "ORDER BY #{options[:order]} " if options[:order] 
    1010  
     1040  
    10111041          add_limit!(sql, options) if using_limitable_reflections?(reflections) 
    1012  
     1042  
    10131043          return sanitize_sql(sql) 
    10141044        end 
    1015  
    1016         def add_limited_ids_condition!(sql, options
    1017           unless (id_list = select_limited_ids_list(options)).empty? 
     1045  
     1046        def add_limited_ids_condition!(sql, options, reflections
     1047          unless (id_list = select_limited_ids_list(options, reflections)).empty? 
    10181048            sql << "#{condition_word(sql)} #{table_name}.#{primary_key} IN (#{id_list}) " 
    10191049          end 
    10201050        end 
    1021  
    1022         def select_limited_ids_list(options
     1051  
     1052        def select_limited_ids_list(options, reflections
    10231053          connection.select_values( 
    1024             construct_finder_sql_for_association_limiting(options), 
     1054            construct_finder_sql_for_association_limiting(options, reflections), 
    10251055            "#{name} Load IDs For Limited Eager Loading" 
    10261056          ).collect { |id| connection.quote(id) }.join(", ") 
    10271057        end 
    1028  
    1029         def construct_finder_sql_for_association_limiting(options) 
    1030           raise(ArgumentError, "Limited eager loads and conditions on the eager tables is incompatible") if include_eager_conditions?(options) 
    1031            
    1032           sql = "SELECT #{primary_key} FROM #{table_name} " 
     1058  
     1059        def construct_finder_sql_for_association_limiting(options, reflections) 
     1060          #sql = "SELECT DISTINCT #{table_name}.#{primary_key} FROM #{table_name} " 
     1061          sql = "SELECT " 
     1062          sql << "DISTINCT #{table_name}." if include_eager_conditions?(options) || include_eager_order?(options) 
     1063          sql << "#{primary_key} FROM #{table_name} " 
     1064           
     1065          if include_eager_conditions?(options) || include_eager_order?(options) 
     1066            sql << reflections.collect { |reflection| association_join(reflection) }.to_s 
     1067            sql << "#{options[:joins]} " if options[:joins] 
     1068          end 
     1069           
    10331070          add_conditions!(sql, options[:conditions]) 
    10341071          sql << "ORDER BY #{options[:order]} " if options[:order] 
     
    10431080          conditions.scan(/(\w+)\.\w+/).flatten.any? do |condition_table_name| 
    10441081            condition_table_name != table_name 
     1082          end 
     1083        end 
     1084         
     1085        def include_eager_order?(options) 
     1086          order = options[:order] 
     1087          return false unless order 
     1088          order.scan(/(\w+)\.\w+/).flatten.any? do |order_table_name| 
     1089            order_table_name != table_name 
    10451090          end 
    10461091        end 
  • trunk/activerecord/lib/active_record/base.rb

    r3533 r3553  
    496496      end 
    497497 
    498       # Returns the number of records that meet the +conditions+. Zero is returned if no records match. Example: 
    499       #   Product.count "sales > 1" 
    500       def count(conditions = nil, joins = nil) 
    501         sql  = "SELECT COUNT(*) FROM #{table_name} " 
    502         sql << " #{joins} " if joins 
    503         add_conditions!(sql, conditions) 
    504         count_by_sql(sql) 
     498      # Count operates using three different approaches.  
     499      # 
     500      # * Count all: By not passing any parameters to count, it will return a count of all the rows for the model. 
     501      # * Count by conditions or joins: For backwards compatibility, you can pass in +conditions+ and +joins+ as individual parameters. 
     502      # * Count using options will find the row count matched by the options used. 
     503      # 
     504      # The last approach, count using options, accepts an option hash as the only parameter. The options are: 
     505      # 
     506      # * <tt>:conditions</tt>: An SQL fragment like "administrator = 1" or [ "user_name = ?", username ]. See conditions in the intro. 
     507      # * <tt>:joins</tt>: An SQL fragment for additional joins like "LEFT JOIN comments ON comments.post_id = id". (Rarely needed). 
     508      #   The records will be returned read-only since they will have attributes that do not correspond to the table's columns. 
     509      # * <tt>:include</tt>: Named associations that should be loaded alongside using LEFT OUTER JOINs. The symbols named refer 
     510      #   to already defined associations. When using named associations count returns the number DISTINCT items for the model you're counting. 
     511      #   See eager loading under Associations. 
     512      # 
     513      # Examples for counting all: 
     514      #   Person.count         # returns the total count of all people 
     515      # 
     516      # Examples for count by +conditions+ and +joins+ (for backwards compatibility): 
     517      #   Person.count("age > 26")  # returns the number of people older than 26 
     518      #   Person.find("age > 26 AND job.salary > 60000", "LEFT JOIN jobs on jobs.person_id = person.id") # returns the total number of rows matching the conditions and joins fetched by SELECT COUNT(*). 
     519      # 
     520      # Examples for count with options: 
     521      #   Person.count(:conditions => "age > 26") 
     522      #   Person.count(:conditions => "age > 26 AND job.salary > 60000", :include => :job) # because of the named association, it finds the DISTINCT count using LEFT OUTER JOIN. 
     523      #   Person.count(:conditions => "age > 26 AND job.salary > 60000", :joins => "LEFT JOIN jobs on jobs.person_id = person.id") # finds the number of rows matching the conditions and joins.  
     524      def count(*args)  
     525        options = {} 
     526         
     527        #For backwards compatibility, we need to handle both count(conditions=nil, joins=nil) or count(options={}). 
     528        if args.size >= 0 and args.size <= 2 
     529          if args.first.is_a?(Hash) 
     530            options = args.first 
     531            #should we verify the options hash??? 
     532          else 
     533            #Handle legacy paramter options: def count(conditions=nil, joins=nil)  
     534            options.merge!(:conditions => args[0]) if args.length > 0 
     535            options.merge!(:joins => args[1]) if args.length > 1 
     536          end 
     537        else 
     538          raise(ArgumentError, "Unexpected parameters passed to count(*args): expected either count(conditions=nil, joins=nil) or count(options={})") 
     539        end 
     540         
     541        options[:include] ? count_with_associations(options) : count_by_sql(construct_counter_sql(options)) 
     542      end 
     543       
     544      def construct_counter_sql(options) 
     545        sql  = "SELECT COUNT("  
     546        sql << "DISTINCT " if options[:distinct] 
     547        sql << "#{table_name}.#{primary_key}) FROM #{table_name} " 
     548        sql << " #{options[:joins]} " if options[:joins] 
     549        add_conditions!(sql, options[:conditions]) 
     550        sql 
    505551      end 
    506552 
  • trunk/activerecord/lib/active_record/connection_adapters/abstract_adapter.rb

    r3219 r3553  
    3838      def supports_migrations? 
    3939        false 
     40      end 
     41       
     42      # Does this adapter support using DISTINCT within COUNT?  This is +true+ 
     43      # for all adapters except sqlite. 
     44      def supports_count_distinct? 
     45        true 
    4046      end 
    4147 
  • trunk/activerecord/lib/active_record/connection_adapters/sqlite_adapter.rb

    r3294 r3553  
    9898      def supports_migrations? #:nodoc: 
    9999        true 
     100      end 
     101       
     102      def supports_count_distinct? #:nodoc: 
     103        false 
    100104      end 
    101105 
  • trunk/activerecord/test/associations_go_eager_test.rb

    r3254 r3553  
    121121 
    122122  def test_eager_with_has_many_and_limit_and_conditions_array_on_the_eagers 
    123     assert_raises(ArgumentError) do 
    124       posts = Post.find(:all, :include => [ :author, :comments ], :limit => 2, :conditions => [ "authors.name = ?", 'David' ]) 
    125     end 
     123    posts = Post.find(:all, :include => [ :author, :comments ], :limit => 2, :conditions => [ "authors.name = ?", 'David' ]) 
     124    assert_equal 2, posts.size 
     125     
     126    count = Post.count(:include => [ :author, :comments ], :limit => 2, :conditions => [ "authors.name = ?", 'David' ]) 
     127    assert_equal count, posts.size 
    126128  end 
    127129 
     
    142144 
    143145  def test_eager_with_has_many_and_limit_and_conditions_on_the_eagers 
    144     assert_raises(ArgumentError) do 
    145       posts = authors(:david).posts.find(:all,  
    146         :include    => :comments,  
    147         :conditions => "comments.body like 'Normal%' OR comments.#{QUOTED_TYPE}= 'SpecialComment'", 
    148         :limit      => 2 
    149       ) 
    150     end 
     146    posts = authors(:david).posts.find(:all,  
     147      :include    => :comments,  
     148      :conditions => "comments.body like 'Normal%' OR comments.#{QUOTED_TYPE}= 'SpecialComment'", 
     149      :limit      => 2 
     150    ) 
     151    assert_equal 2, posts.size 
     152     
     153    count = Post.count( 
     154      :include    => [ :comments, :author ],  
     155      :conditions => "authors.name = 'David' AND (comments.body like 'Normal%' OR comments.#{QUOTED_TYPE}= 'SpecialComment')", 
     156      :limit      => 2 
     157    ) 
     158    assert_equal count, posts.size 
    151159  end 
    152160 
  • trunk/activerecord/test/base_test.rb

    r3452 r3553  
    10571057    end 
    10581058    assert_equal res, res2 
     1059     
     1060    res3 = res + 1 
     1061    assert_nothing_raised do 
     1062      res3 = Post.count(:conditions => "posts.#{QUOTED_TYPE} = 'Post'", 
     1063                        :joins => "LEFT JOIN comments ON posts.id=comments.post_id") 
     1064    end 
     1065    assert_equal res, res3 
    10591066  end 
    10601067