active_record_calculator 0.0.4 → 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- data/README +61 -0
- data/active_record_calculator.gemspec +2 -2
- data/lib/active_record_calculator.rb +10 -8
- data/lib/active_record_calculator/calculator_proxy.rb +91 -15
- data/lib/active_record_calculator/column.rb +21 -0
- data/lib/active_record_calculator/error.rb +5 -0
- data/lib/active_record_calculator/group_operation.rb +21 -0
- data/lib/active_record_calculator/operation.rb +4 -0
- data/lib/active_record_calculator/updater_proxy.rb +86 -0
- data/lib/active_record_calculator/version.rb +1 -1
- metadata +12 -5
data/README
ADDED
@@ -0,0 +1,61 @@
|
|
1
|
+
Use active_record_calculator to perform subgrouping of aggregate calculations
|
2
|
+
|
3
|
+
Example:
|
4
|
+
|
5
|
+
calculator = Purchases.calculator(:conditions => "created_at > '2011-07-01'", :group => "user_id") do |c|
|
6
|
+
c.count :id, "total"
|
7
|
+
c.cnt :id, "expensive", "price > 100" #cnt is an alias for count
|
8
|
+
c.sum :price, "cheap_spending", "price < 100"
|
9
|
+
c.avg :price, "cheap_average", "price < 100" #avg is an alias for average
|
10
|
+
c.min :price, "least"
|
11
|
+
c.max :price, "most" #max is an alias for maximum
|
12
|
+
c.col :item_name #col is an alias for column; adds a column to the result
|
13
|
+
end
|
14
|
+
|
15
|
+
calculator.calculate
|
16
|
+
|
17
|
+
OR
|
18
|
+
|
19
|
+
calculator = Purchases.calculator(:conditions => "created_at > '2011-07-01'", :group => "user_id")
|
20
|
+
calculator.count :id, "total"
|
21
|
+
calculator.cnt :id, "expensive", "price > 100"
|
22
|
+
calculator.sum :price, "cheap_spending", "price < 100"
|
23
|
+
calculator.avg :price, "cheap_average", "price < 100"
|
24
|
+
calculator.min :price, "least"
|
25
|
+
calculator.max :price, "most"
|
26
|
+
calculator.calculate
|
27
|
+
|
28
|
+
When adding operations, the calculator expects the format
|
29
|
+
|
30
|
+
method(column, alias, sub_group_condition)
|
31
|
+
|
32
|
+
The calculate method yields an array of hashes with the aliases as keys and results as values
|
33
|
+
Group columns are automatically included
|
34
|
+
|
35
|
+
Example:
|
36
|
+
|
37
|
+
>> calc = Transaction.calculator(:conditions => "transactions.user_id = 55555", :group => "transactions.user_id, bonus") do |c|
|
38
|
+
?> c.count :id, "transactions_count"
|
39
|
+
>> c.count :id, "approved_bonus_count", "status = 'approved' and bonus = true"
|
40
|
+
>> c.count :id, "approved_offers_count", "status = 'approved' and bonus = false"
|
41
|
+
>> end
|
42
|
+
...
|
43
|
+
>> calc.calculate
|
44
|
+
=> [{"approved_bonus_count"=>0, "group_column_1"=>"55555", "group_column_2"=>"0", "transactions_count"=>34, "approved_offers_count"=>0}, {"approved_bonus_count"=>17, "group_column_1"=>"55555", "group_column_2"=>"1", "transactions_count"=>18, "approved_offers_count"=>17}]
|
45
|
+
|
46
|
+
You can use statement to see the sql created
|
47
|
+
|
48
|
+
The calculator can be used for fast direct sql updates. A calculator needs to be created where all the operations have aliases
|
49
|
+
that have a respective column name in the update table:
|
50
|
+
|
51
|
+
calculator = Purchases.calculator(:conditions => "created_at > '2011-07-01'", :group => "user_id")
|
52
|
+
calculator.count :id, "total_purchases"
|
53
|
+
calculator.cnt :id, "expensive_purchases", "price > 100"
|
54
|
+
calculator.sum :price, "cheap_spending", "price < 100"
|
55
|
+
calculator.avg :price, "cheap_average", "price < 100"
|
56
|
+
calculator.min :price, "least_purchase"
|
57
|
+
calculator.max :price, "most_purchase"
|
58
|
+
|
59
|
+
calculator.update(:purchase_history, :user_id) #the tables are joined by the second argument and the first grouping column
|
60
|
+
|
61
|
+
Update only works with mysql adapters currently
|
@@ -22,10 +22,10 @@ Gem::Specification.new do |s|
|
|
22
22
|
# s.add_development_dependency "rspec"
|
23
23
|
# s.add_runtime_dependency "rest-client"
|
24
24
|
if Gem::Version.new(Gem::VERSION) >= Gem::Version.new('1.2.0') then
|
25
|
-
s.add_runtime_dependency('activerecord')
|
25
|
+
s.add_runtime_dependency('activerecord', ">=0")
|
26
26
|
s.add_development_dependency("rspec")
|
27
27
|
else
|
28
|
-
s.add_dependency('activerecord')
|
28
|
+
s.add_dependency('activerecord', ">=0")
|
29
29
|
s.add_development_dependency("rspec")
|
30
30
|
end
|
31
31
|
end
|
@@ -1,6 +1,12 @@
|
|
1
|
+
require 'active_record'
|
1
2
|
require "active_record_calculator/version"
|
2
3
|
require "active_record_calculator/calculator_proxy"
|
4
|
+
require "active_record_calculator/updater_proxy"
|
3
5
|
require "active_record_calculator/operation"
|
6
|
+
require "active_record_calculator/group_operation"
|
7
|
+
require "active_record_calculator/column"
|
8
|
+
require "active_record_calculator/error"
|
9
|
+
require "bigdecimal"
|
4
10
|
|
5
11
|
module ActiveRecordCalculator
|
6
12
|
def self.included(base)
|
@@ -8,14 +14,10 @@ module ActiveRecordCalculator
|
|
8
14
|
end
|
9
15
|
|
10
16
|
module ClassMethods
|
11
|
-
def
|
12
|
-
|
13
|
-
yield
|
14
|
-
|
15
|
-
end
|
16
|
-
|
17
|
-
def calculator
|
18
|
-
CalculatorProxy.new(self)
|
17
|
+
def calculator(options = {}, &blk)
|
18
|
+
c = CalculatorProxy.new(self, options)
|
19
|
+
yield c if blk
|
20
|
+
c
|
19
21
|
end
|
20
22
|
end
|
21
23
|
end
|
@@ -4,7 +4,8 @@ module ActiveRecordCalculator
|
|
4
4
|
@klass = klass
|
5
5
|
@operations = []
|
6
6
|
@columns = []
|
7
|
-
@
|
7
|
+
@group_operations = []
|
8
|
+
find(finder_options)
|
8
9
|
end
|
9
10
|
|
10
11
|
def col(column_name, as = nil)
|
@@ -36,31 +37,106 @@ module ActiveRecordCalculator
|
|
36
37
|
end
|
37
38
|
alias :minimum :min
|
38
39
|
|
40
|
+
def table_name
|
41
|
+
@klass.table_name
|
42
|
+
end
|
43
|
+
|
44
|
+
def columns
|
45
|
+
@columns
|
46
|
+
end
|
47
|
+
|
48
|
+
def operations
|
49
|
+
@operations
|
50
|
+
end
|
51
|
+
|
52
|
+
def group_operations
|
53
|
+
@group_operations
|
54
|
+
end
|
55
|
+
|
56
|
+
def update_key
|
57
|
+
@group_operations.first ? @group_operations.first.name : nil
|
58
|
+
end
|
59
|
+
|
39
60
|
def calculate
|
40
|
-
|
41
|
-
|
42
|
-
|
43
|
-
|
61
|
+
result = select_all(statement)
|
62
|
+
result.collect do |row|
|
63
|
+
@operations.each do |op|
|
64
|
+
row[op.name] = type_cast(row[op.name])
|
65
|
+
end
|
66
|
+
row
|
67
|
+
end
|
44
68
|
end
|
45
69
|
|
46
|
-
def
|
47
|
-
@
|
70
|
+
def connection
|
71
|
+
@klass.connection
|
48
72
|
end
|
49
73
|
|
50
|
-
def
|
51
|
-
|
52
|
-
|
53
|
-
|
74
|
+
def select_all(query)
|
75
|
+
connection.select_all(query)
|
76
|
+
end
|
77
|
+
|
78
|
+
def update(table, foreign_key)
|
79
|
+
unless connection.adapter_name =~ /^mysql/i
|
80
|
+
raise UnsupportedAdapterError, "Updates with the database adapter is not supported."
|
81
|
+
end
|
82
|
+
updater = UpdaterProxy.new(table, foreign_key, self)
|
83
|
+
updater
|
84
|
+
end
|
85
|
+
|
86
|
+
def find(finder_options = {})
|
87
|
+
finder_options.symbolize_keys!
|
88
|
+
@finder_options = finder_options.except(:select, :include, :from, :readonly, :lock)
|
89
|
+
end
|
90
|
+
|
91
|
+
def statement
|
92
|
+
add_group_operations
|
93
|
+
sql = @klass.send(:construct_finder_sql, @finder_options.except(:update_key))
|
94
|
+
sql.gsub(/^SELECT\s+\*/i, select)
|
54
95
|
end
|
55
96
|
|
56
97
|
private
|
57
98
|
|
58
|
-
def
|
59
|
-
|
60
|
-
|
99
|
+
def select
|
100
|
+
s = []
|
101
|
+
s += @group_operations.collect {|op| op.build_select}
|
102
|
+
s += @columns.collect {|col| op.build_select}
|
103
|
+
s += @operations.collect {|op| op.build_select(@klass)}
|
104
|
+
"SELECT " + s.join(",\n")
|
105
|
+
end
|
106
|
+
|
107
|
+
def groupings
|
108
|
+
@groupings ||= @group_operations.collect(&:name)
|
109
|
+
end
|
110
|
+
|
111
|
+
def type_cast(data)
|
112
|
+
if data =~ /^\d+$/
|
113
|
+
data.to_i
|
114
|
+
elsif data =~ /^\d+\.\d+$/
|
115
|
+
BigDecimal(data)
|
61
116
|
else
|
62
|
-
|
117
|
+
data
|
118
|
+
end
|
119
|
+
end
|
120
|
+
|
121
|
+
def add_column(column_name, as)
|
122
|
+
@columns << Column.new(column_name,as)
|
123
|
+
end
|
124
|
+
|
125
|
+
def add_group_operations
|
126
|
+
@group_operations = []
|
127
|
+
return unless @finder_options[:group]
|
128
|
+
group_attrs = @finder_options[:group].to_s.split(',')
|
129
|
+
if @finder_options[:for_update]
|
130
|
+
@finder_options[:group] = group_attrs.first
|
131
|
+
group_attrs = [group_attrs.first]
|
132
|
+
end
|
133
|
+
group_attrs.each_with_index do |grp, i|
|
134
|
+
grp.downcase!
|
135
|
+
grp.strip!
|
136
|
+
grp_alias = "group_column_#{i+1}"
|
137
|
+
@group_operations << GroupOperation.new(grp, grp_alias)
|
63
138
|
end
|
139
|
+
@group_operations.uniq!
|
64
140
|
end
|
65
141
|
|
66
142
|
def add_operation(op, column_name, as, options)
|
@@ -0,0 +1,21 @@
|
|
1
|
+
module ActiveRecordCalculator
|
2
|
+
class Column
|
3
|
+
|
4
|
+
def initialize(name, as)
|
5
|
+
@name = name
|
6
|
+
@as = as
|
7
|
+
end
|
8
|
+
|
9
|
+
def build_select
|
10
|
+
"#{@name} AS #{@as || @name}"
|
11
|
+
end
|
12
|
+
|
13
|
+
def inspect
|
14
|
+
@name
|
15
|
+
end
|
16
|
+
|
17
|
+
def alias_name
|
18
|
+
@as
|
19
|
+
end
|
20
|
+
end
|
21
|
+
end
|
@@ -0,0 +1,21 @@
|
|
1
|
+
module ActiveRecordCalculator
|
2
|
+
class GroupOperation
|
3
|
+
|
4
|
+
def initialize(column, as)
|
5
|
+
@column = column
|
6
|
+
@as = as
|
7
|
+
end
|
8
|
+
|
9
|
+
def build_select
|
10
|
+
"#{@column} AS #{@as}"
|
11
|
+
end
|
12
|
+
|
13
|
+
def inspect
|
14
|
+
@column
|
15
|
+
end
|
16
|
+
|
17
|
+
def name
|
18
|
+
@as
|
19
|
+
end
|
20
|
+
end
|
21
|
+
end
|
@@ -0,0 +1,86 @@
|
|
1
|
+
module ActiveRecordCalculator
|
2
|
+
class UpdaterProxy
|
3
|
+
def initialize(table, foreign_key, calculator_proxy)
|
4
|
+
@table = table
|
5
|
+
set_calculator(calculator_proxy)
|
6
|
+
@key = foreign_key
|
7
|
+
valid_columns?
|
8
|
+
valid_update_key?
|
9
|
+
end
|
10
|
+
|
11
|
+
def statement
|
12
|
+
start = %{UPDATE #{table}
|
13
|
+
INNER JOIN
|
14
|
+
(#{calculator.statement}
|
15
|
+
) AS sub_#{subquery_table} ON sub_#{subquery_table}.group_column_1 = #{table}.#{key}
|
16
|
+
SET\n}
|
17
|
+
start += calculation_columns.collect do |col|
|
18
|
+
"#{table}.#{col} = sub_#{subquery_table}.#{col}"
|
19
|
+
end.join(",\n")
|
20
|
+
start
|
21
|
+
end
|
22
|
+
|
23
|
+
private
|
24
|
+
|
25
|
+
def connection
|
26
|
+
calculator.connection
|
27
|
+
end
|
28
|
+
|
29
|
+
def calculator
|
30
|
+
@calculator
|
31
|
+
end
|
32
|
+
|
33
|
+
def key
|
34
|
+
"#{remove_table(@key)}"
|
35
|
+
end
|
36
|
+
|
37
|
+
def set_calculator(calculator_proxy)
|
38
|
+
calculator_proxy.send(:add_group_operations)
|
39
|
+
@calculator = calculator_proxy
|
40
|
+
end
|
41
|
+
|
42
|
+
def table
|
43
|
+
@table
|
44
|
+
end
|
45
|
+
|
46
|
+
def subquery_table
|
47
|
+
calculator.table_name
|
48
|
+
end
|
49
|
+
|
50
|
+
def update_columns
|
51
|
+
@update_columns ||= connection.columns(table).collect {|obj| obj.name.to_s}
|
52
|
+
end
|
53
|
+
|
54
|
+
def calculation_columns
|
55
|
+
@calculation_columns ||= (calculator.columns + calculator.operations.collect(&:name))
|
56
|
+
end
|
57
|
+
|
58
|
+
def invalid_columns
|
59
|
+
@invalid_columns ||= calculation_columns - update_columns
|
60
|
+
end
|
61
|
+
|
62
|
+
def invalid_columns_sentence
|
63
|
+
start = invalid_columns[0..-2].join(', ')
|
64
|
+
if invalid_columns.length > 1
|
65
|
+
start += ' and ' + invalid_columns[-1..-1].to_s
|
66
|
+
end
|
67
|
+
start
|
68
|
+
end
|
69
|
+
|
70
|
+
def valid_columns?
|
71
|
+
raise InvalidColumnError, "Can not resolve update column(s) #{invalid_columns.to_sentence} for table #{update_table}" unless invalid_columns.empty?
|
72
|
+
true
|
73
|
+
end
|
74
|
+
|
75
|
+
def remove_table(s)
|
76
|
+
s.to_s.split('.').last
|
77
|
+
end
|
78
|
+
|
79
|
+
def valid_update_key?
|
80
|
+
unless calculator.update_key
|
81
|
+
raise NoUpdateKeyError, "No valid update key was provided for table #{subquery_table}"
|
82
|
+
end
|
83
|
+
true
|
84
|
+
end
|
85
|
+
end
|
86
|
+
end
|
metadata
CHANGED
@@ -1,13 +1,13 @@
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
2
2
|
name: active_record_calculator
|
3
3
|
version: !ruby/object:Gem::Version
|
4
|
-
hash:
|
4
|
+
hash: 27
|
5
5
|
prerelease:
|
6
6
|
segments:
|
7
7
|
- 0
|
8
|
+
- 1
|
8
9
|
- 0
|
9
|
-
|
10
|
-
version: 0.0.4
|
10
|
+
version: 0.1.0
|
11
11
|
platform: ruby
|
12
12
|
authors:
|
13
13
|
- Grady Griffin
|
@@ -15,7 +15,8 @@ autorequire:
|
|
15
15
|
bindir: bin
|
16
16
|
cert_chain: []
|
17
17
|
|
18
|
-
date: 2012-01-
|
18
|
+
date: 2012-01-31 00:00:00 -05:00
|
19
|
+
default_executable:
|
19
20
|
dependencies:
|
20
21
|
- !ruby/object:Gem::Dependency
|
21
22
|
name: activerecord
|
@@ -57,12 +58,18 @@ extra_rdoc_files: []
|
|
57
58
|
files:
|
58
59
|
- .gitignore
|
59
60
|
- Gemfile
|
61
|
+
- README
|
60
62
|
- Rakefile
|
61
63
|
- active_record_calculator.gemspec
|
62
64
|
- lib/active_record_calculator.rb
|
63
65
|
- lib/active_record_calculator/calculator_proxy.rb
|
66
|
+
- lib/active_record_calculator/column.rb
|
67
|
+
- lib/active_record_calculator/error.rb
|
68
|
+
- lib/active_record_calculator/group_operation.rb
|
64
69
|
- lib/active_record_calculator/operation.rb
|
70
|
+
- lib/active_record_calculator/updater_proxy.rb
|
65
71
|
- lib/active_record_calculator/version.rb
|
72
|
+
has_rdoc: true
|
66
73
|
homepage: ""
|
67
74
|
licenses: []
|
68
75
|
|
@@ -92,7 +99,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement
|
|
92
99
|
requirements: []
|
93
100
|
|
94
101
|
rubyforge_project: active_record_calculator
|
95
|
-
rubygems_version: 1.
|
102
|
+
rubygems_version: 1.4.2
|
96
103
|
signing_key:
|
97
104
|
specification_version: 3
|
98
105
|
summary: ActiveRecord Calculations done faster
|