Skip to content

Instantly share code, notes, and snippets.

@avit
Created July 21, 2012 09:22
Show Gist options
  • Save avit/3155195 to your computer and use it in GitHub Desktop.
Save avit/3155195 to your computer and use it in GitHub Desktop.
ActiveRecord::Relation::Calculations#count fails on grouping with select
gem 'activerecord', '4.0.0.beta'
require 'active_record'
require 'logger'
ActiveRecord::Base.logger = Logger.new(STDOUT)
ActiveRecord::Base.logger.level = Logger::INFO
ActiveRecord::Base.establish_connection( :adapter => 'sqlite3',
:database => ':memory:' )
ActiveRecord::Schema.define do
create_table "guests", :force => true do |t|
t.string :name
end
create_table "reservations", :force => true do |t|
t.references :guest
t.date :check_in
end
end
class Guest < ActiveRecord::Base
has_many :reservations
def self.by_first_check_in
joins(:reservations).group("guests.id")
.select("guests.*")
.select("MIN(reservations.check_in) first_check_in")
.order("first_check_in")
end
def self.wrapped
inner_scope = self.all # 3.2: use self.scoped
self.unscoped do
from(inner_scope.as(table_name))
end
end
end
class Reservation < ActiveRecord::Base
belongs_to :guest
end
a = Guest.create(:name => 'A')
b = Guest.create(:name => 'B')
a.reservations.create(:check_in => Date.new(2012,7,23))
a.reservations.create(:check_in => Date.new(2012,8,24))
b.reservations.create(:check_in => Date.new(2012,7,21))
b.reservations.create(:check_in => Date.new(2012,8,22))
puts "#" * 80
puts <<-README
Active Record #{ActiveRecord::VERSION::STRING}
GOAL: get a count of rows from a query that includes pseudo columns and grouping.
When counting with grouping in the relation, ActiveRecord tries to return group
counts instead of a total count and drops the SELECT on the floor. If there are
pseudo-columns in the SELECT for grouping, this breaks the whole query.
Also, if it worked, this would return a hash instead of a fixnum. There doesn't
seem to be a way to get the total of returned rows, ignoring the grouping.
* count should be usable to return a Fixnum count even when grouping is
needed in the query.
* relation.count and relation.all.count should have the same semantics.
* count should work directly on any relation, so the relation can still be
used as a chainable scope.
* If I send the relation to the view, the paginator helper may call .count on
it, so I have to use .all defensively in the right places. This conflicts
with late-binding of things like table presenters that may want to chain
additional scopes e.g. to change sorting. This is a conflict: paginator wants
.count, table presenter wants a chainable scope.
Suggestion for Rails 4: The Calculations#count method is too ambiguous and assumes
too much. Change Calculations#count method to only return groups if :group option
is passed directly to the count method, i.e. do not trigger grouped counts based on
other group_values in the relation. That, or split #count and #count_grouped
into separate methods. This might make it easier to do the right thing with the
query builder.
README
relation = Guest.by_first_check_in
puts
puts "# relation.load"
result = relation.load
puts result.map{ |g| [g.name, g.first_check_in].join(" = ") }
puts
puts "# relation.to_a.count"
puts relation.to_a.count
puts
puts "# relation.load.count (expect: 2; throws an error)"
begin
puts relation.load.count
rescue StandardError => e
puts "fail: #{e}"
end
puts
puts "# relation.count (expect: 2; throws an error)"
begin
puts relation.count
rescue StandardError => e
puts "fail: #{e}"
end
puts
puts "#" * 80
puts <<-README
The above shows how regular count breaks. This is my workaround by hiding the
joins and groups in a subquery:
README
puts
puts "# relation.wrapped.load (in subquery)"
result = relation.wrapped.load
puts result.map{ |g| [g.name, g.first_check_in].join(" = ") }
puts
puts %{# relation.wrapped.where("first_check_in > '2012-07-21'").load}
result = relation.wrapped.where("first_check_in > '2012-07-21'").load
puts result.map{ |g| [g.name, g.first_check_in].join(" = ") }
puts
puts "# relation.wrapped.count (in subquery)"
puts relation.wrapped.count
puts
puts "# relation.wrapped.load.count"
puts relation.wrapped.load.count
puts
puts %{# relation.wrapped.where("first_check_in > '2012-07-21'").count}
puts relation.wrapped.where("first_check_in > '2012-07-21'").count
puts
puts "# relation.wrapped.group(:first_check_in).count"
puts relation.wrapped.group(:first_check_in).count
@HotFusionMan
Copy link

Unfortunately, we had a method that was being handed a Relation instance constructed with left outer joins in its query, and this technique did not handle that (Arel throws a NoMethodError on "left"). Someone else on my team discovered that ActiveRecord::Base has an undocumented #length method (which loads the relation under the hood -- syntactically a little nicer than having to explicitly call .all before .size or .count) that we're now using in the case where .count returns a Hash (which indicates that the relation was formed via GROUP BY).

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment