This project is archived and is in readonly mode.
Enumerable#sum not called on some association collections
Reported by Chris Schumann | March 17th, 2009 @ 01:40 PM | in 2.x
In Rails 2.3.2, Enumerable#sum is not always called on arrays that are association collections. If you have a simple parent-child relationship, the code parent.children.sum calls the association collection sum, but if you generate the collection with find, invoking its sum method calls Enumerable#sum.
There's a bit of code - patience is appreciated.
# With Rails 2.3.2
# Commands to re-create the issue:
$ rails sum
$ cd sum
sum $ script/generate model parent
sum $ script/generate model child parent_id:integer
# app/models/child.rb:
class Child < ActiveRecord::Base
belongs_to :parent
end
# app/models/parent.rb:
class Parent < ActiveRecord::Base
has_many :children
end
sum $ rake db:migrate
sum $ script/console
>> p=Parent.new
=> #<Parent id: nil, created_at: nil, updated_at: nil>
>> p.save
=> true
>> c=Child.new
=> #<Child id: nil, parent_id: nil, created_at: nil, updated_at: nil>
>> c.parent=p
=> #<Parent id: 1, created_at: "2009-03-17 01:57:30", updated_at: "2009-03-17 01:57:30">
>> c
=> #<Child id: nil, parent_id: 1, created_at: nil, updated_at: nil>
>> c.save
=> true
# Now the fun part. Find all children with find.
>> cs=Child.find(:all, :conditions => 'parent_id = 1')
=> [#<Child id: 1, parent_id: 1, created_at: "2009-03-17 01:57:54", updated_at: "2009-03-17 01:57:54">]
# Compute sum of id's
>> cs.sum(&:id)
=> 1
# Now collect them through the association
>> ds=Parent.find(1).children
=> [#<Child id: 1, parent_id: 1, created_at: "2009-03-17 01:57:54", updated_at: "2009-03-17 01:57:54">]
# Try to sum the id's
>> ds.sum(&:id)
ArgumentError: wrong number of arguments (1 for 2)
# Complete error copied below
# Try to make it call the desired method (Enumerable#sum):
>> ds.sum(0,&:id)
=> "0"
# This does it
>> ds.to_a.sum(&:id)
=> 1
# The two arrays appear to be equivalent
>> cs.class
=> Array
>> ds.class
=> Array
>> cs.size
=> 1
>> ds.size
=> 1
>> cs[0].class
=> Child(id: integer, parent_id: integer, created_at: datetime, updated_at: datetime)
>> ds[0].class
=> Child(id: integer, parent_id: integer, created_at: datetime, updated_at: datetime)
>> cs==ds
=> true
>> cs===ds
=> true
# Complete error output
>> ds.sum(&:id)
ArgumentError: wrong number of arguments (1 for 2)
from /usr/lib/ruby/gems/1.8/gems/activerecord-2.3.2/lib/active_record/associations/association_collection.rb:373:in `calculate'
from /usr/lib/ruby/gems/1.8/gems/activerecord-2.3.2/lib/active_record/associations/association_collection.rb:373:in `send'
from /usr/lib/ruby/gems/1.8/gems/activerecord-2.3.2/lib/active_record/associations/association_collection.rb:373:in `method_missing'
from /usr/lib/ruby/gems/1.8/gems/activerecord-2.3.2/lib/active_record/base.rb:2148:in `with_scope'
from /usr/lib/ruby/gems/1.8/gems/activerecord-2.3.2/lib/active_record/associations/association_proxy.rb:206:in `send'
from /usr/lib/ruby/gems/1.8/gems/activerecord-2.3.2/lib/active_record/associations/association_proxy.rb:206:in `with_scope'
from /usr/lib/ruby/gems/1.8/gems/activerecord-2.3.2/lib/active_record/associations/association_collection.rb:371:in `method_missing'
from /usr/lib/ruby/gems/1.8/gems/activerecord-2.3.2/lib/active_record/associations/association_collection.rb:155:in `sum'
from (irb):4
from :0
Comments and changes to this ticket
-
Chris Schumann March 17th, 2009 @ 03:24 PM
A failing unit test:
# test/unit/child_test.rb require 'test_helper' class ChildTest < ActiveSupport::TestCase test "should sum ids" do p = Parent.new p.children.build p.save # This one passes assert p.children.find(:all).sum(&:id) > 0 # This one fails assert p.children.sum(&:id) > 0 end end
-
Will Bryant May 6th, 2009 @ 04:15 AM
This is the expected current behaviour. Calling some_has_many.sum calls the Calculations module sum, which passes the string argument you give it to the database - so you should use "p.children.sum('id')".
A patch to support loading the target array and delegating the calculation when passing a block instead of a string (which is effectively what you're doing above) to the calculation method (sum etc.) would be welcome.
-
CancelProfileIsBroken August 6th, 2009 @ 02:25 PM
- Tag changed from 2.3.2, enumerable, sum to 2.3.2, bugmash, enumerable, sum
-
Adam Keys August 8th, 2009 @ 05:01 PM
Will, I'm wondering if changing some_has_many.sum to load all records is too sugary. Case in point, I can look at this and think "hey, this is obviously loading a bunch of records into memory":
person.children.all.sum(&:id)
And that this is going to do some aggregate cleverness on the database and avoid huge loads:
Person.all(&:id)
But the changing this to load all the records on the has_many muddies the waters:
person.children.sum(&:id)
Does this do something clever with aggregates or does it load a bunch of objects? Or am I missing something?
-
Dan Pickett August 8th, 2009 @ 08:01 PM
-1 - I disagree that this is a bug -
this is by design to me - the finder should always return an array. conditions should be supplied in the sum call.
-
dira August 9th, 2009 @ 03:10 PM
I agree - this is design, not a bug. -1 for BugMash.
The requested syntactic sugar for has_many has two facets:
- it would cause 'huge loads' from the database if the 'many' are not loaded yet - it would be more efficient if the data is loaded already :)I favor implementation of the suggestion because the some_has_many should support the Enumerable interface.
-
Elad Meidar August 9th, 2009 @ 03:37 PM
-1 here too, seem that it will require too much db attention to avoid huge loads when the association is not memo'd, chaining to #all seem to be the way i would go for.
-
José Valim August 9th, 2009 @ 03:42 PM
-1 For me this is by design too.
I also disagree supporting the Enumerable interface, since they are different things. In Chris case, for example, the proper way to calculate the sum, would be:
@parent.children.sum('id')
If we allow people to do this:
@parent.children.sum(&:id)
We are giving a shortcut to write non performant code. If you really want to do that, invoking to array looks like the way to go (and it also make obvious what is happening):
@parent.children.to_a.sum(&:id)
Could be handy a documentation patch, to explain the differences between AssoicationProxy#sum and Array#sum.
-
Pratik August 9th, 2009 @ 11:58 PM
- State changed from new to invalid
-
José Valim August 9th, 2009 @ 11:59 PM
- Tag changed from 2.3.2, bugmash, enumerable, sum to 2.3.2, enumerable, sum
Create your profile
Help contribute to this project by taking a few moments to create your personal profile. Create your profile »
<h2 style="font-size: 14px">Tickets have moved to Github</h2>
The new ticket tracker is available at <a href="https://github.com/rails/rails/issues">https://github.com/rails/rails/issues</a>