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 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