active_record_calculator 0.0.4 → 0.1.0

Sign up to get free protection for your applications and to get access to all the features.
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 calculate_many(options = {}, &blk)
12
- calculator = CalculatorProxy.new(self, options)
13
- yield calculator
14
- calculator.calculate
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
- @finder_options = finder_options.except(:order, :select)
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
- sql = @klass.send(:construct_finder_sql, @finder_options)
41
- sql.gsub!(/^SELECT \*/, select)
42
- @operations = []
43
- @klass.find_by_sql(sql)
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 set_finder_options(finder_options = {})
47
- @finder_options = finder_options.except(:select)
70
+ def connection
71
+ @klass.connection
48
72
  end
49
73
 
50
- def select
51
- s = "SELECT\n"
52
- s += @columns.join(', ') + "\n"
53
- s += @operations.collect {|op| op.build_select(@klass)}.join(",\n")
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 add_column(column_name, as)
59
- if as
60
- @columns << column_name
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
- @columns << "#{column_name} AS #{as}"
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,5 @@
1
+ module ActiveRecordCalculator
2
+ class NoUpdateKeyError < StandardError; end
3
+ class InvalidColumnError < StandardError; end
4
+ class UnsupportedAdapterError < StandardError; end
5
+ 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
@@ -27,6 +27,10 @@ module ActiveRecordCalculator
27
27
  "#{@as} => #{@op}"
28
28
  end
29
29
 
30
+ def name
31
+ @as
32
+ end
33
+
30
34
  private
31
35
 
32
36
  def default
@@ -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
@@ -1,3 +1,3 @@
1
1
  module ActiveRecordCalculator
2
- VERSION = "0.0.4"
2
+ VERSION = "0.1.0"
3
3
  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: 23
4
+ hash: 27
5
5
  prerelease:
6
6
  segments:
7
7
  - 0
8
+ - 1
8
9
  - 0
9
- - 4
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-27 00:00:00 Z
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.8.15
102
+ rubygems_version: 1.4.2
96
103
  signing_key:
97
104
  specification_version: 3
98
105
  summary: ActiveRecord Calculations done faster