abacus_count 0.0.1
Sign up to get free protection for your applications and to get access to all the features.
- data/.gitignore +4 -0
- data/Gemfile +4 -0
- data/README.markdown +37 -0
- data/Rakefile +9 -0
- data/abacus_count.gemspec +25 -0
- data/lib/abacus_count.rb +11 -0
- data/lib/abacus_count/calculations.rb +36 -0
- data/lib/abacus_count/relation.rb +11 -0
- data/lib/abacus_count/version.rb +3 -0
- data/spec/database.rb +4 -0
- data/spec/lib/calculations_spec.rb +47 -0
- data/spec/spec_helper.rb +15 -0
- data/spec/support/models.rb +19 -0
- metadata +104 -0
data/.gitignore
ADDED
data/Gemfile
ADDED
data/README.markdown
ADDED
@@ -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.
|
data/Rakefile
ADDED
@@ -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
|
data/lib/abacus_count.rb
ADDED
@@ -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
|
data/spec/database.rb
ADDED
@@ -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
|
data/spec/spec_helper.rb
ADDED
@@ -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
|