abacus_count 0.0.1

Sign up to get free protection for your applications and to get access to all the features.
@@ -0,0 +1,4 @@
1
+ *.gem
2
+ .bundle
3
+ Gemfile.lock
4
+ pkg/*
data/Gemfile ADDED
@@ -0,0 +1,4 @@
1
+ source "http://rubygems.org"
2
+
3
+ # Specify your gem's dependencies in abacus_count.gemspec
4
+ gemspec
@@ -0,0 +1,37 @@
1
+ # Abacus Count #
2
+
3
+ ## Synopsis ##
4
+
5
+ `ActiveRecord::Base#count` and other calculations as subqueries. Instead of nice Rails grouping calculation feature, uses subqueries to return the result with any complex query.
6
+
7
+ There is a use case when ActiveRecord calculations fail on a valid relation. It's when you use alias values you select, and then use aliases in where or having conditions. That happens because ActiveRecord does calculations by throwing away select values. And when really **do** need to use custom aliases outside select, you definitely **do not** want this kind of optimization. More on that bug in this [pull request](https://github.com/rails/rails/pull/1969).
8
+
9
+ While it's not fixed, we can use an ultimate way of preventing any calculation issues, suggested by abacus_count. I mean using subqueries.
10
+
11
+ ## Installation ##
12
+
13
+ In your `Gemfile`:
14
+
15
+ ``` ruby
16
+ gem "abacus_count"
17
+ ```
18
+
19
+ and then run `bundle`.
20
+
21
+ ## Usage ##
22
+
23
+ You now have `abacus` scope on any relation. This scope extends a relation with a calculations patch. With it, each calculation will be performed through subquery and so they won't fail in any case.
24
+
25
+ ``` ruby
26
+ users = User.select("users.id, avg(transactions.amount) as avg_amount").joins(:transactions).group("user_id").having("avg_amount >= 15")
27
+ users.count # will fail
28
+ users.abacus.count # will do
29
+
30
+ # with Kaminari
31
+ users.page(params[:page]).per(10) # will fail in count
32
+ users.abacus.page(params[:page]).per(10) # will do
33
+ ```
34
+
35
+ ### Caveats ###
36
+
37
+ In `abacus` calculations never return a hash, always a total result. Sometimes, this is just what you want, however.
@@ -0,0 +1,9 @@
1
+ require 'bundler/gem_tasks'
2
+
3
+ require 'rspec/core/rake_task'
4
+
5
+ task :default => :spec
6
+ RSpec::Core::RakeTask.new(:spec) do |t|
7
+ end
8
+
9
+
@@ -0,0 +1,25 @@
1
+ # -*- encoding: utf-8 -*-
2
+ $:.push File.expand_path("../lib", __FILE__)
3
+ require "abacus_count/version"
4
+
5
+ Gem::Specification.new do |s|
6
+ s.name = "abacus_count"
7
+ s.version = AbacusCount::VERSION
8
+ s.authors = ["Dmitriy Kiriyenko"]
9
+ s.email = ["dmitriy.kiriyenko@gmail.com"]
10
+ s.homepage = "https://github.com/dmitriy-kiriyenko/abacus_count"
11
+ s.summary = %q{ActiveRecord::Base#count and other calculations as subqueries}
12
+ s.description = %q{ActiveRecord::Base#count and other calculations as subqueries. Instead of nice Rails grouping calculation feature, \
13
+ uses subqueries to return the result with any complex query}
14
+
15
+ s.rubyforge_project = "abacus_count"
16
+
17
+ s.add_dependency "activerecord", ">= 3.0.0"
18
+ s.add_development_dependency "rspec", ">= 2.0.0"
19
+ s.add_development_dependency "sqlite3"
20
+
21
+ s.files = `git ls-files`.split("\n")
22
+ s.test_files = `git ls-files -- {test,spec,features}/*`.split("\n")
23
+ s.executables = `git ls-files -- bin/*`.split("\n").map{ |f| File.basename(f) }
24
+ s.require_paths = ["lib"]
25
+ end
@@ -0,0 +1,11 @@
1
+ require "abacus_count/version"
2
+
3
+ require "active_record"
4
+ require "active_record/base" # TODO: that's because ActiveRecord::Relation does not load delegation core_ext from active_support.
5
+ # Perhaps, Rails are waiting for a patch
6
+
7
+ require "abacus_count/relation"
8
+
9
+ module AbacusCount
10
+ # Your code goes here...
11
+ end
@@ -0,0 +1,36 @@
1
+ module AbacusCount
2
+ module Calculations
3
+ def execute_simple_calculation(operation, column_name, distinct)
4
+ execute_subquery_calculation(operation, column_name, distinct)
5
+ end
6
+
7
+ def execute_grouped_calculation(operation, column_name, distinct)
8
+ execute_subquery_calculation(operation, column_name, distinct)
9
+ end
10
+
11
+ def execute_subquery_calculation(operation, column_name, distinct)
12
+ relation = reorder(nil)
13
+
14
+ column_alias = Arel.sql("#{operation}_column")
15
+ subquery_alias = Arel.sql("subquery_for_#{operation}")
16
+
17
+ aliased_column = aggregate_column(column_name == :all ? 1 : column_name).as(column_alias)
18
+ relation.select_values += [aliased_column]
19
+
20
+ subquery = build_subquery(relation, subquery_alias)
21
+
22
+ sm = Arel::SelectManager.new relation.engine
23
+ select_value = operation_over_aggregate_column(column_alias, operation, distinct)
24
+ query_builder = sm.project(select_value).from(subquery)
25
+
26
+ type_cast_calculated_value(@klass.connection.select_value(query_builder.to_sql), column_for(column_name), operation)
27
+ end
28
+
29
+ def build_subquery(relation, aliaz)
30
+ # Since arel 2.1.1 it will look like
31
+ # relation.arel.as(subquery_alias)
32
+ Arel::Nodes::As.new(Arel::Nodes::Grouping.new(relation.arel.ast), Arel::Nodes::SqlLiteral.new(aliaz))
33
+ end
34
+
35
+ end
36
+ end
@@ -0,0 +1,11 @@
1
+ require 'abacus_count/calculations'
2
+
3
+ module AbacusCount
4
+ module Relation
5
+ def abacus
6
+ extending(AbacusCount::Calculations)
7
+ end
8
+ end
9
+ end
10
+
11
+ ActiveRecord::Relation.send(:include, AbacusCount::Relation)
@@ -0,0 +1,3 @@
1
+ module AbacusCount
2
+ VERSION = "0.0.1"
3
+ end
@@ -0,0 +1,4 @@
1
+ ActiveRecord::Base.configurations = {'test' => {:adapter => 'sqlite3', :database => ':memory:'}}
2
+ ActiveRecord::Base.establish_connection('test')
3
+
4
+ ActiveRecord::Migration.verbose = false unless ENV.has_key?('DEBUG')
@@ -0,0 +1,47 @@
1
+ require 'spec_helper'
2
+
3
+ describe "calculations" do
4
+ before(:all) do
5
+ User.create(:name => "John", :email => "john@example.com").tap do |u|
6
+ u.transactions.create :amount => 10
7
+ u.transactions.create :amount => 15
8
+ u.transactions.create :amount => 20
9
+ end
10
+
11
+ User.create(:name => "Mike").tap do |u|
12
+ u.transactions.create :amount => 5
13
+ u.transactions.create :amount => 10
14
+ u.transactions.create :amount => 15
15
+ end
16
+
17
+ User.create(:name => "Jane").tap do |u|
18
+ u.transactions.create :amount => 10
19
+ u.transactions.create :amount => 20
20
+ u.transactions.create :amount => 30
21
+ end
22
+ end
23
+
24
+ let(:grouped_scope) do
25
+ User.select("users.id, avg(transactions.amount) as avg_amount").joins(:transactions).group("user_id").having("avg_amount >= 15")
26
+ end
27
+
28
+ let(:simple_scope) do
29
+ User.select("users.id, email AS my_email_alias").where("my_email_alias IS NOT NULL")
30
+ end
31
+
32
+ it "should return users' count with having condition" do
33
+ grouped_scope.abacus.count.should == 2
34
+ end
35
+
36
+ it "should return users' count with having condition counted by column" do
37
+ grouped_scope.abacus.count(:email).should == 1
38
+ end
39
+
40
+ it "should return sum of all user's amount with having condition" do
41
+ grouped_scope.abacus.sum(:amount) == 105
42
+ end
43
+
44
+ it "should return users' count with where condition" do
45
+ simple_scope.abacus.count.should == 1
46
+ end
47
+ end
@@ -0,0 +1,15 @@
1
+ require 'rubygems'
2
+ require 'bundler/setup'
3
+
4
+ Bundler.require(:default, :development)
5
+
6
+ # Establishes database connection
7
+ require File.join(File.dirname(__FILE__), 'database')
8
+
9
+ # Requires supporting files with custom matchers and macros, etc,
10
+ # in ./support/ and its subdirectories.
11
+ Dir[File.join(File.dirname(__FILE__), "support", "**", "*.rb")].each {|f| require f}
12
+
13
+ RSpec.configure do |config|
14
+ # some (optional) config here
15
+ end
@@ -0,0 +1,19 @@
1
+ class User < ActiveRecord::Base
2
+ has_many :transactions
3
+ end
4
+
5
+ class Transaction < ActiveRecord::Base
6
+ belongs_to :user
7
+ end
8
+
9
+ ActiveRecord::Schema.define(:version => 1) do
10
+ create_table :users do |t|
11
+ t.string :name
12
+ t.string :email
13
+ end
14
+
15
+ create_table :transactions do |t|
16
+ t.integer :amount
17
+ t.integer :user_id
18
+ end
19
+ end
metadata ADDED
@@ -0,0 +1,104 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: abacus_count
3
+ version: !ruby/object:Gem::Version
4
+ prerelease:
5
+ version: 0.0.1
6
+ platform: ruby
7
+ authors:
8
+ - Dmitriy Kiriyenko
9
+ autorequire:
10
+ bindir: bin
11
+ cert_chain: []
12
+
13
+ date: 2011-07-07 00:00:00 Z
14
+ dependencies:
15
+ - !ruby/object:Gem::Dependency
16
+ name: activerecord
17
+ prerelease: false
18
+ requirement: &id001 !ruby/object:Gem::Requirement
19
+ none: false
20
+ requirements:
21
+ - - ">="
22
+ - !ruby/object:Gem::Version
23
+ version: 3.0.0
24
+ type: :runtime
25
+ version_requirements: *id001
26
+ - !ruby/object:Gem::Dependency
27
+ name: rspec
28
+ prerelease: false
29
+ requirement: &id002 !ruby/object:Gem::Requirement
30
+ none: false
31
+ requirements:
32
+ - - ">="
33
+ - !ruby/object:Gem::Version
34
+ version: 2.0.0
35
+ type: :development
36
+ version_requirements: *id002
37
+ - !ruby/object:Gem::Dependency
38
+ name: sqlite3
39
+ prerelease: false
40
+ requirement: &id003 !ruby/object:Gem::Requirement
41
+ none: false
42
+ requirements:
43
+ - - ">="
44
+ - !ruby/object:Gem::Version
45
+ version: "0"
46
+ type: :development
47
+ version_requirements: *id003
48
+ description: |-
49
+ ActiveRecord::Base#count and other calculations as subqueries. Instead of nice Rails grouping calculation feature, \
50
+ uses subqueries to return the result with any complex query
51
+ email:
52
+ - dmitriy.kiriyenko@gmail.com
53
+ executables: []
54
+
55
+ extensions: []
56
+
57
+ extra_rdoc_files: []
58
+
59
+ files:
60
+ - .gitignore
61
+ - Gemfile
62
+ - README.markdown
63
+ - Rakefile
64
+ - abacus_count.gemspec
65
+ - lib/abacus_count.rb
66
+ - lib/abacus_count/calculations.rb
67
+ - lib/abacus_count/relation.rb
68
+ - lib/abacus_count/version.rb
69
+ - spec/database.rb
70
+ - spec/lib/calculations_spec.rb
71
+ - spec/spec_helper.rb
72
+ - spec/support/models.rb
73
+ homepage: https://github.com/dmitriy-kiriyenko/abacus_count
74
+ licenses: []
75
+
76
+ post_install_message:
77
+ rdoc_options: []
78
+
79
+ require_paths:
80
+ - lib
81
+ required_ruby_version: !ruby/object:Gem::Requirement
82
+ none: false
83
+ requirements:
84
+ - - ">="
85
+ - !ruby/object:Gem::Version
86
+ version: "0"
87
+ required_rubygems_version: !ruby/object:Gem::Requirement
88
+ none: false
89
+ requirements:
90
+ - - ">="
91
+ - !ruby/object:Gem::Version
92
+ version: "0"
93
+ requirements: []
94
+
95
+ rubyforge_project: abacus_count
96
+ rubygems_version: 1.8.4
97
+ signing_key:
98
+ specification_version: 3
99
+ summary: ActiveRecord::Base#count and other calculations as subqueries
100
+ test_files:
101
+ - spec/database.rb
102
+ - spec/lib/calculations_spec.rb
103
+ - spec/spec_helper.rb
104
+ - spec/support/models.rb