calculate-all 0.2.2 → 0.3.1

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.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 3cd324cd911d0d4998540ce59b30addac7a8e9842676f1adbf75b5bf6adee1c0
4
- data.tar.gz: 26ff146cce10d207334d6232e72dae5d8d558246fd47f58da7571a6e84bbba7a
3
+ metadata.gz: f15872602a35e41a3619150837e69840e82da1d2a42bd058a7c6986092024a97
4
+ data.tar.gz: f3665c7b64a083c5f24fd861e6ef22a5dde177e4a68eab9638449e26f89e2dc7
5
5
  SHA512:
6
- metadata.gz: 1570c270af925b14130207b5905235d02f19960fc83f2ae189c67cd482a06538b341a031279daea5c8dc01933eb5303c0cc5db30a7fb706a4ff111c79bac4ec4
7
- data.tar.gz: 38f29b21183c14901b61790e5e4a69ce70918e2c99bac4ba4a714a5ee8a1be4466bfcc6ab6cfd93e43a098041d8cbedfa4206c2adc1d6ce766111cdbd0db26f1
6
+ metadata.gz: 5d7ed4f530ff56be9378644b209add40b415bedf363425599a10a7d3fc14b1d0cb0920dd6744f5c334ae83f0ec9296b161d8bc387a2e4f93c1a849d7764e571a
7
+ data.tar.gz: 164b353805bf7fdd12e10087186c20a805231f68922c4d46a42e7c8f02307165d36592cbcf0c67d2a27f61e6dbb43c9cd7ab2934b9a64112ef239cf4e50edf05
@@ -6,8 +6,10 @@ jobs:
6
6
  fail-fast: false
7
7
  matrix:
8
8
  include:
9
- - ruby: "3.1"
9
+ - ruby: "3.4"
10
10
  gemfile: Gemfile
11
+ - ruby: "3.1"
12
+ gemfile: gemfiles/activerecord70.gemfile
11
13
  - ruby: "3.0"
12
14
  gemfile: gemfiles/activerecord61.gemfile
13
15
  - ruby: "2.7"
@@ -18,7 +20,7 @@ jobs:
18
20
  gemfile: gemfiles/activerecord51.gemfile
19
21
  - ruby: "2.6"
20
22
  gemfile: gemfiles/activerecord50.gemfile
21
- - ruby: "2.5"
23
+ - ruby: "2.3"
22
24
  gemfile: gemfiles/activerecord42.gemfile
23
25
  runs-on: ubuntu-latest
24
26
  env:
data/CHANGELOG.md CHANGED
@@ -1,3 +1,14 @@
1
+ ## 0.3.1
2
+
3
+ * Fix some arguments exceptions
4
+
5
+ ## 0.3.0
6
+
7
+ * Allow expression shortcuts as attribute values too for renaming
8
+ * Allow grouping expressions to be returned in rows too
9
+ * Breaking change: only single *string* expression argument is returning unwrapped rows now.
10
+ Single expression shortcut like `:count` will be expanded to `{count: value}` rows.
11
+
1
12
  ## 0.2.2
2
13
 
3
14
  * Added support for Groupdate 4+ (Andrew <acekane1@gmail.com>)
data/Gemfile CHANGED
@@ -4,10 +4,14 @@ gemspec
4
4
 
5
5
  gem "rake"
6
6
  gem "minitest"
7
- gem "activerecord", "~> 7.0.0"
8
- gem "groupdate", "~> 5.2.0"
7
+ gem "minitest-reporters"
8
+ gem "activerecord", "~> 8.0.0"
9
+ gem "groupdate", "~> 6"
9
10
  gem "pg"
10
11
  gem "mysql2"
11
- gem "sqlite3", "~> 1.4"
12
+ gem "sqlite3", ">= 2.1"
13
+
14
+ gem "irb"
15
+ gem "ostruct"
12
16
 
13
17
  gem "standardrb", require: false
data/README.md CHANGED
@@ -1,50 +1,61 @@
1
1
  # CalculateAll
2
2
 
3
3
  Provides the `#calculate_all` method for your Active Record models, scopes and relations.
4
- It's a small addition to Active Record's `#count`, `#maximum`, `#minimum`, `#average` and `#sum`.
4
+ It's a small addition to Active Record's `#count`, `#maximum`, `#minimum`, `#average`, `#sum`
5
+ and `#calculate`.
5
6
  It allows you to fetch all of the above, as well as other aggregate function results,
6
7
  in a single request, with support for grouping.
7
8
 
8
- Currently tested with Postgres, MySQL and sqlite3, ruby >= 2.3, rails >= 4, groupdate >= 4.
9
+ Should be useful for dashboards, timeseries stats, and charts.
10
+
11
+ Currently tested with PostgreSQL, MySQL and SQLite3, ruby >= 2.3, rails >= 4, groupdate >= 4.
9
12
 
10
13
  ## Usage
11
14
 
15
+ (example SQL snippets are given for PostgreSQL)
16
+
12
17
  ```ruby
13
- stats = Order.group(:department_id).group(:payment_method).calculate_all(
18
+ stats = Order.group(:department_id).group(:payment_method).order(:payment_method).calculate_all(
19
+ :payment_method,
14
20
  :count,
15
- :count_distinct_user_id,
16
21
  :price_max,
17
22
  :price_min,
18
23
  :price_avg,
19
- price_median: 'percentile_cont(0.5) within group (order by price desc)'
24
+ total_users: :count_distinct_user_id,
25
+ price_median: "percentile_cont(0.5) within group (order by price asc)",
26
+ plan_ids: "array_agg(distinct plan_id order by plan_id)",
27
+ earnings: "sum(price) filter (where status = 'paid')"
20
28
  )
21
- #
22
- # (2.2ms) SELECT department_id, payment_method, percentile_cont(0.5) within group (order by price desc),
23
- # COUNT(*), COUNT(DISTINCT user_id), MAX(price), MIN(price), AVG(price) FROM "orders" GROUP BY "department_id", "payment_method"
24
- #
29
+ # Order Pluck (20.0ms) SELECT "orders"."department_id", "payment_method", COUNT(*), MAX(price), MIN(price), AVG(price),
30
+ # COUNT(DISTINCT user_id), percentile_cont(0.5) within group (order by price asc),
31
+ # array_agg(distinct plan_id order by plan_id), sum(price) filter (where status = 'paid')
32
+ # FROM "orders" GROUP BY "orders"."department_id", "payment_method" ORDER BY "payment_method" ASC
25
33
  # => {
26
- # [1, "cash"] => {
34
+ # [1, "card"] => {
35
+ # payment_method: "card",
27
36
  # count: 10,
28
- # count_distinct_user_id: 5,
29
37
  # price_max: 500,
30
38
  # price_min: 100,
31
39
  # price_avg: 0.3e3,
32
- # price_median: 0.4e3
40
+ # total_users: 5,
41
+ # price_median: 0.4e3,
42
+ # plan_ids: [4, 7, 12],
43
+ # earnings: 2340
33
44
  # },
34
- # [1, "card"] => {
45
+ # [1, "cash"] => {
35
46
  # ...
36
47
  # }
37
48
  # }
38
49
  ```
39
50
 
40
- (median example works in Postgres only, but check out very cool https://github.com/ankane/active_median)
41
-
42
51
  ## Rationale
43
52
 
44
53
  Active Record makes it really easy to use most common database aggregate functions like COUNT(), MAX(), MIN(), AVG(), SUM().
45
- But there's a whole world of other [powerful functions](http://www.postgresql.org/docs/current/functions-aggregate.html) in
46
- Postgres, which I can’t recommend enough, especially if you’re working with statistics or business intelligence.
47
- MySQL has some useful ones [as well](https://dev.mysql.com/doc/refman/9.3/en/aggregate-functions.html).
54
+ But there's a whole world of other aggregate functions in
55
+ [PostgreSQL](http://www.postgresql.org/docs/current/functions-aggregate.html),
56
+ [MySQL](https://dev.mysql.com/doc/refman/9.3/en/aggregate-functions.html)
57
+ and [SQLite](https://www.sqlite.org/lang_aggfunc.html)
58
+ which I can’t recommend enough, especially if you’re working with statistics or business intelligence.
48
59
 
49
60
  Also, in many cases, you’ll need multiple metrics at once. Typically, the database performs a full scan of the table for each metric.
50
61
  However, it can calculate all of them in a single scan and a single request.
@@ -53,26 +64,23 @@ However, it can calculate all of them in a single scan and a single request.
53
64
 
54
65
  ## Arguments
55
66
 
56
- `#calculate_all` accepts a list of expression aliases and/or expression mappings.
57
- It can be a single string of SQL,
67
+ `#calculate_all` accepts a single SQL expression with aggregate functions,
58
68
 
59
69
  ```ruby
60
- Model.calculate_all('SUM(price) / COUNT(DISTINCT user_id)')
70
+ Model.calculate_all('CAST(SUM(price) as decimal) / COUNT(DISTINCT user_id)')
61
71
  ```
62
72
 
63
- a hash of expressions with arbitrary symbol keys
73
+ or arbitrary symbols and keyword arguments with SQL snippets, aggregate function shortcuts or previously given grouping values.
64
74
 
65
75
  ```ruby
66
- Model.calculate_all(total: 'COUNT(*)', average_spendings: 'SUM(price) / COUNT(DISTINCT user_id)')
76
+ Model.group(:currency).calculate_all(
77
+ :average_price, :currency, total: :sum_price, average_spendings: 'SUM(price)::decimal / COUNT(DISTINCT user_id)'
78
+ )
67
79
  ```
68
- and/or a list of one or more symbols without expressions, in which case `#calculate_all` tries to guess
69
- what you wanted from it.
70
80
 
71
- ```ruby
72
- Model.calculate_all(:count, :average_price, :sum_price)
73
- ```
81
+ For convenience, `calculate_all(:count, :avg_column)` is the same as `calculate_all(count: :count, avg_column: :avg_column)`
74
82
 
75
- It's not so smart right now, but here's a cheatsheet:
83
+ Here's a cheatsheet of recognized shortcuts:
76
84
 
77
85
  | symbol | would fetch
78
86
  |------------------------------------------------------------------------|------------
@@ -84,6 +92,8 @@ It's not so smart right now, but here's a cheatsheet:
84
92
  | `:avg_column1`, `:column1_avg`, `:average_column1`, `:column1_average` | `AVG(column1)`
85
93
  | `:sum_column1`, `:column1_sum` | `SUM(column1)`
86
94
 
95
+ Other functions are a bit too database specific, and are better to be given with an explicit SQL snippet.
96
+
87
97
  Please don't put values from unverified sources (like HTML form or javascript call) into expression list,
88
98
  it could result in malicious SQL injection.
89
99
 
@@ -92,23 +102,20 @@ it could result in malicious SQL injection.
92
102
  `#calculate_all` tries to mimic magic of Active Record's `#group`, `#count` and `#pluck`
93
103
  so result type depends on arguments and on groupings.
94
104
 
95
- If you have no `group()` on underlying scope, `#calculate_all` will return just one result.
105
+ If you have no `group()` on underlying scope, `#calculate_all` will return just one row.
96
106
 
97
107
  ```ruby
98
- # Same as Order.distinct.count(:user_id), so probably a useless example.
99
- # But you can use any expression with aggregate functions there.
100
- Order.calculate_all('COUNT(DISTINCT user_id)')
101
- # => 50
108
+ Order.calculate_all(:price_sum)
109
+ # => {price_sum: 123500}
102
110
  ```
103
111
 
104
112
  If you have a single `group()`, it will return a hash of results with simple keys.
105
113
 
106
114
  ```ruby
107
- # Again, Order.group(:department_id).distinct.count(:user_id) would do the same.
108
115
  Order.group(:department_id).calculate_all(:count_distinct_user_id)
109
116
  # => {
110
- # 1 => 20,
111
- # 2 => 10,
117
+ # 1 => {count_distinct_user_id: 20},
118
+ # 2 => {count_distinct_user_id: 10},
112
119
  # ...
113
120
  # }
114
121
  ```
@@ -116,25 +123,36 @@ Order.group(:department_id).calculate_all(:count_distinct_user_id)
116
123
  If you have two or more groupings, each result will have an array as a key.
117
124
 
118
125
  ```ruby
119
- Order.group(:department_id).group(:department_method).calculate_all(:count_distinct_user_id)
126
+ Order.group(:department_id).group(:payment_method).calculate_all(:count)
120
127
  # => {
121
- # [1, "cash"] => 5,
122
- # [1, "card"] => 15,
123
- # [2, "cash"] => 1,
128
+ # [1, "cash"] => {count: 5},
129
+ # [1, "card"] => {count: 15},
130
+ # [2, "cash"] => {count: 1},
124
131
  # ...
125
132
  # }
126
133
  ```
127
134
 
128
- If you provide only one argument to `#calculate_all`, its calculated value will be returned as-is.
129
- Otherwise, the results will be returned as hash(es) with symbol keys.
135
+ If you provide only one *string* argument to `#calculate_all`, its calculated value will be returned as-is.
136
+ This is just to make grouped companion to `Model.group(...).count` and friends, but for arbitrary expressions
137
+ with aggregate functions.
138
+
139
+ ```ruby
140
+ Order.group(:payment_method).calculate_all('CAST(SUM(price) AS decimal) / COUNT(DISTINCT user_id)')
141
+ # => {
142
+ # "card" => 0.524e3,
143
+ # "cash" => 0.132e3
144
+ # }
145
+ ```
130
146
 
131
- so, `Order.calculate_all(:count)` will return just a single integer, but
147
+ Otherwise, the results will be returned as hash(es) with symbol keys.
132
148
 
133
149
  ```ruby
134
- Order.group(:department_id).group(:payment_method).calculate_all(:min_price, expr1: 'count(distinct user_id)')
150
+ Order.group(:department_id).group(:payment_method).calculate_all(
151
+ :min_price, type: :payment_method, expr1: 'count(distinct user_id)'
152
+ )
135
153
  # => {
136
- # [1, 'cash'] => {min_price: 100, expr1: 5},
137
- # [1, 'card'] => {min_price: 150, expr2: 15},
154
+ # [1, 'cash'] => {min_price: 100, type: 'cash', expr1: 5},
155
+ # [1, 'card'] => {min_price: 150, type: 'card', expr1: 15},
138
156
  # ...
139
157
  # }
140
158
  ```
@@ -151,7 +169,7 @@ Order.group(:country_id).calculate_all(:count, :avg_price) { |count:, avg_price:
151
169
  # 2 => "10 orders, 200 dollars average"
152
170
  # }
153
171
 
154
- Order.group(:country_id).calculate_all(:avg_price) { |avg_price| avg_price.to_i }
172
+ Order.group(:country_id).calculate_all("AVG(price)") { |avg_price| avg_price.to_i }
155
173
  # => {
156
174
  # 1 => 120,
157
175
  # 2 => 200
@@ -159,6 +177,17 @@ Order.group(:country_id).calculate_all(:avg_price) { |avg_price| avg_price.to_i
159
177
 
160
178
  Order.calculate_all(:count, :max_price, &OpenStruct.method(:new))
161
179
  # => #<OpenStruct max_price=500, count=15>
180
+
181
+ Stats = Data.define(:count, :max_price) do
182
+ # needed only for groupdate to provide defaults for empty periods
183
+ def initialize(count: 0, max_price: nil) = super
184
+ end
185
+ Order.group_by_year(:created_at).calculate_all(*Stats.members, &Stats.method(:new))
186
+ # => {
187
+ # Wed, 01 Jan 2014 => #<data Stats count=2, max_price=700>,
188
+ # Thu, 01 Jan 2015 => #<data Stats count=0, max_price=nil>,
189
+ # Fri, 01 Jan 2016 => #<data Stats count=3, max_price=800>
190
+ # }
162
191
  ```
163
192
 
164
193
  ## groupdate compatibility
@@ -198,9 +227,17 @@ Or install it yourself as:
198
227
 
199
228
  After checking out the repo, run `bin/setup` to install dependencies. Then, run `rake test` to run the tests.
200
229
  Run `BUNDLE_GEMFILE=gemfiles/activerecord60.gemfile bundle` then `BUNDLE_GEMFILE=gemfiles/activerecord60.gemfile rake`
201
- to test agains specific active record version.
230
+ to test against specific active record version.
231
+
232
+ To experiment you can load a test database and jump to IRB with
233
+
234
+ ```sh
235
+ rake VERBOSE=1 CONSOLE=1 TESTOPTS="--name=test_console" test:postgresql
236
+ ```
202
237
 
203
- To install this gem onto your local machine, run `bundle exec rake install`. To release a new version, update the version number in `version.rb`, and then run `bundle exec rake release`, which will create a git tag for the version, push git commits and tags, and push the `.gem` file to [rubygems.org](https://rubygems.org).
238
+ To install this gem onto your local machine, run `bundle exec rake install`. To release a new version, update the version
239
+ number in `version.rb`, and then run `bundle exec rake release`, which will create a git tag for the version, push git commits and tags,
240
+ and push the `.gem` file to [rubygems.org](https://rubygems.org).
204
241
 
205
242
  ## Contributing
206
243
 
data/Rakefile CHANGED
@@ -1,10 +1,22 @@
1
1
  require "bundler/gem_tasks"
2
2
  require "rake/testtask"
3
3
 
4
- Rake::TestTask.new(:test) do |t|
5
- t.libs << "test"
6
- t.libs << "lib"
7
- t.test_files = FileList["test/**/*_test.rb"]
4
+ ADAPTERS = %w[postgresql mysql sqlite]
5
+
6
+ ADAPTERS.each do |adapter|
7
+ namespace :test do
8
+ task("env:#{adapter}") { ENV["ADAPTER"] = adapter }
9
+
10
+ Rake::TestTask.new(adapter => "env:#{adapter}") do |t|
11
+ t.description = "Run tests for #{adapter}"
12
+ t.libs << "test"
13
+ t.libs << "lib"
14
+ t.test_files = FileList["test/**/*_test.rb"]
15
+ end
16
+ end
8
17
  end
9
18
 
19
+ desc "Run all adapter tests"
20
+ task test: ADAPTERS.map { |adapter| "test:#{adapter}" }
21
+
10
22
  task default: :test
@@ -18,5 +18,7 @@ Gem::Specification.new do |spec|
18
18
  spec.executables = spec.files.grep(%r{^exe/}) { |f| File.basename(f) }
19
19
  spec.require_paths = ["lib"]
20
20
 
21
- spec.add_dependency "activesupport"
21
+ spec.required_ruby_version = '>= 2.3.0'
22
+
23
+ spec.add_dependency "activesupport", ">= 4.0.0"
22
24
  end
@@ -4,6 +4,7 @@ gemspec path: ".."
4
4
 
5
5
  gem "rake"
6
6
  gem "minitest", "~> 5.0"
7
+ gem "minitest-reporters"
7
8
  gem "activerecord", "~> 4.2.0"
8
9
  gem "groupdate", "~> 3.0.0"
9
10
  gem "pg", "~> 0.15"
@@ -4,6 +4,7 @@ gemspec path: ".."
4
4
 
5
5
  gem "rake"
6
6
  gem "minitest"
7
+ gem "minitest-reporters"
7
8
  gem "activerecord", "~> 5.0.0"
8
9
  gem "groupdate", "~> 4.0.0"
9
10
  gem "pg"
@@ -4,6 +4,7 @@ gemspec path: ".."
4
4
 
5
5
  gem "rake"
6
6
  gem "minitest"
7
+ gem "minitest-reporters"
7
8
  gem "activerecord", "~> 5.1.0"
8
9
  gem "groupdate", "~> 5.0.0"
9
10
  gem "pg"
@@ -4,6 +4,7 @@ gemspec path: ".."
4
4
 
5
5
  gem "rake"
6
6
  gem "minitest"
7
+ gem "minitest-reporters"
7
8
  gem "activerecord", "~> 5.2.0"
8
9
  gem "groupdate", "~> 5.0.0"
9
10
  gem "pg"
@@ -4,6 +4,7 @@ gemspec path: ".."
4
4
 
5
5
  gem "rake"
6
6
  gem "minitest"
7
+ gem "minitest-reporters"
7
8
  gem "activerecord", "~> 6.0.0"
8
9
  gem "groupdate", "~> 5.0.0"
9
10
  gem "pg"
@@ -4,6 +4,7 @@ gemspec path: ".."
4
4
 
5
5
  gem "rake"
6
6
  gem "minitest"
7
+ gem "minitest-reporters"
7
8
  gem "activerecord", "~> 6.1.0"
8
9
  gem "groupdate", "~> 5.0.0"
9
10
  gem "pg"
@@ -0,0 +1,12 @@
1
+ source "https://rubygems.org"
2
+
3
+ gemspec path: ".."
4
+
5
+ gem "rake"
6
+ gem "minitest"
7
+ gem "minitest-reporters"
8
+ gem "activerecord", "~> 7.0.0"
9
+ gem "groupdate", "~> 5.2.0"
10
+ gem "pg"
11
+ gem "mysql2"
12
+ gem "sqlite3", "~> 1.4"
@@ -1,3 +1,3 @@
1
1
  module CalculateAll
2
- VERSION = "0.2.2"
2
+ VERSION = "0.3.1"
3
3
  end
data/lib/calculate-all.rb CHANGED
@@ -3,54 +3,55 @@ require "calculate-all/version"
3
3
 
4
4
  module CalculateAll
5
5
  # Calculates multiple aggregate values on a scope in one request, similarly to #calculate
6
- def calculate_all(*function_aliases, **functions, &block)
7
- # If only one aggregate is given without explicit naming,
6
+ def calculate_all(*expression_shortcuts, **named_expressions, &block)
7
+ # If only one aggregate is given as a string or Arel.sql without explicit naming,
8
8
  # return row(s) directly without wrapping in Hash
9
- if function_aliases.size == 1 && functions.size == 0
9
+ if expression_shortcuts.size == 1 && expression_shortcuts.first.is_a?(String) &&
10
+ named_expressions.size == 0
10
11
  return_plain_values = true
11
12
  end
12
13
 
13
- # Convert the function_aliases to actual SQL
14
- functions.merge!(CalculateAll::Helpers.decode_function_aliases(function_aliases))
14
+ named_expressions = expression_shortcuts.map { |name| [name, name] }.to_h.merge(named_expressions)
15
15
 
16
- # Check if any functions are given
17
- if functions == {}
18
- raise ArgumentError, "provide at least one function to calculate"
16
+ named_expressions.transform_values! do |shortcut|
17
+ Helpers.decode_expression_shortcut(shortcut, group_values)
19
18
  end
20
19
 
21
- columns = (group_values.map(&:to_s) + functions.values).map { |sql| Arel.sql(sql) }
20
+ raise ArgumentError, "provide at least one expression to calculate" if named_expressions.empty?
21
+
22
+ # Some older active_record versions do not allow for repeating expressions in pluck list,
23
+ # and named expressions could contain group values.
24
+ columns = (group_values + named_expressions.values).uniq
25
+ value_mapping = named_expressions.transform_values { |column| columns.index(column) }
26
+ columns.map! { |column| column.is_a?(String) ? Arel.sql(column) : column }
27
+
22
28
  results = {}
23
29
  pluck(*columns).each do |row|
24
- # If pluck called without any groups and with a single argument,
25
- # it will return an array of simple results instead of array of arrays
26
- if functions.size == 1 && group_values.size == 0
27
- row = [row]
28
- end
30
+ # If pluck called with a single argument
31
+ # it will return an array of scalars instead of array of arrays
32
+ row = [row] if columns.size == 1
29
33
 
30
34
  key = if group_values.size == 0
31
35
  :ALL
32
36
  elsif group_values.size == 1
33
37
  # If only one group is provided, the resulting key is just a scalar value
34
- row.shift
38
+ row.first
35
39
  else
36
40
  # if multiple groups, the key will be an array.
37
- row.shift(group_values.size)
41
+ row.first(group_values.size)
38
42
  end
39
43
 
40
- value = if return_plain_values
41
- row.last
42
- else
43
- # it is possible to have more actual group values returned than group_values.size
44
- functions.keys.zip(row.last(functions.size)).to_h
45
- end
44
+ value = value_mapping.transform_values { |index| row[index] }
45
+
46
+ value = value.values.last if return_plain_values
46
47
 
47
48
  results[key] = value
48
49
  end
49
50
 
50
51
  # Additional groupdate magic of filling empty periods with defaults
51
52
  if defined?(Groupdate.process_result)
52
- # Since that hash is the same instance for every backfilled raw, at least
53
- # freeze it to prevent surprize modifications in calling code.
53
+ # Since that hash is the same instance for every backfilled row, at least
54
+ # freeze it to prevent surprise modifications across multiple rows in the calling code.
54
55
  default_value = return_plain_values ? nil : {}.freeze
55
56
  results = Groupdate.process_result(self, results, default_value: default_value)
56
57
  end
@@ -68,12 +69,48 @@ module CalculateAll
68
69
  results
69
70
  end
70
71
  end
72
+
73
+ # module just to not pollute namespace
74
+ module Helpers
75
+ module_function
76
+
77
+ # Convert shortcuts like :count_distinct_id to SQL aggregate functions like 'COUNT(DISTINCT ID)'
78
+ # If shortcut is actually one of the grouping expressions, just return it as-is.
79
+ def decode_expression_shortcut(shortcut, group_values = [])
80
+ case shortcut
81
+ when String
82
+ shortcut
83
+ when *group_values
84
+ shortcut
85
+ when :count
86
+ "COUNT(*)"
87
+ when /^(\w+)_distinct_count$/, /^count_distinct_(\w+)$/
88
+ "COUNT(DISTINCT #{$1})"
89
+ when /^(\w+)_(count|sum|max|min|avg)$/
90
+ "#{$2.upcase}(#{$1})"
91
+ when /^(count|sum|max|min|avg)_(\w+)$/
92
+ "#{$1.upcase}(#{$2})"
93
+ when /^(\w+)_average$/, /^average_(\w+)$/
94
+ "AVG(#{$1})"
95
+ when /^(\w+)_maximum$/, /^maximum_(\w+)$/
96
+ "MAX(#{$1})"
97
+ when /^(\w+)_minimum$/, /^minimum_(\w+)$/
98
+ "MIN(#{$1})"
99
+ else
100
+ raise ArgumentError, "Can't recognize expression shortcut #{shortcut}"
101
+ end
102
+ end
103
+ end
104
+
105
+ module Querying
106
+ # @see CalculateAll#calculate_all
107
+ def calculate_all(*args, **kwargs, &block)
108
+ all.calculate_all(*args, **kwargs, &block)
109
+ end
110
+ end
71
111
  end
72
112
 
73
113
  ActiveSupport.on_load(:active_record) do
74
- require "calculate-all/helpers"
75
- require "calculate-all/querying"
76
-
77
114
  # Make the calculate_all method available for all ActiveRecord::Relations instances
78
115
  ActiveRecord::Relation.include CalculateAll
79
116
 
metadata CHANGED
@@ -1,14 +1,13 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: calculate-all
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.2.2
4
+ version: 0.3.1
5
5
  platform: ruby
6
6
  authors:
7
7
  - Alexey Trofimenko
8
- autorequire:
9
8
  bindir: exe
10
9
  cert_chain: []
11
- date: 2025-05-12 00:00:00.000000000 Z
10
+ date: 2025-05-19 00:00:00.000000000 Z
12
11
  dependencies:
13
12
  - !ruby/object:Gem::Dependency
14
13
  name: activesupport
@@ -16,14 +15,14 @@ dependencies:
16
15
  requirements:
17
16
  - - ">="
18
17
  - !ruby/object:Gem::Version
19
- version: '0'
18
+ version: 4.0.0
20
19
  type: :runtime
21
20
  prerelease: false
22
21
  version_requirements: !ruby/object:Gem::Requirement
23
22
  requirements:
24
23
  - - ">="
25
24
  - !ruby/object:Gem::Version
26
- version: '0'
25
+ version: 4.0.0
27
26
  description: 'Extends Active Record with #calculate_all method'
28
27
  email:
29
28
  - aronaxis@gmail.com
@@ -47,15 +46,13 @@ files:
47
46
  - gemfiles/activerecord52.gemfile
48
47
  - gemfiles/activerecord60.gemfile
49
48
  - gemfiles/activerecord61.gemfile
49
+ - gemfiles/activerecord70.gemfile
50
50
  - lib/calculate-all.rb
51
- - lib/calculate-all/helpers.rb
52
- - lib/calculate-all/querying.rb
53
51
  - lib/calculate-all/version.rb
54
52
  homepage: http://github.com/codesnik/calculate-all
55
53
  licenses:
56
54
  - MIT
57
55
  metadata: {}
58
- post_install_message:
59
56
  rdoc_options: []
60
57
  require_paths:
61
58
  - lib
@@ -63,15 +60,14 @@ required_ruby_version: !ruby/object:Gem::Requirement
63
60
  requirements:
64
61
  - - ">="
65
62
  - !ruby/object:Gem::Version
66
- version: '0'
63
+ version: 2.3.0
67
64
  required_rubygems_version: !ruby/object:Gem::Requirement
68
65
  requirements:
69
66
  - - ">="
70
67
  - !ruby/object:Gem::Version
71
68
  version: '0'
72
69
  requirements: []
73
- rubygems_version: 3.4.19
74
- signing_key:
70
+ rubygems_version: 3.6.2
75
71
  specification_version: 4
76
72
  summary: Fetch from database results of several aggregate functions at once
77
73
  test_files: []
@@ -1,33 +0,0 @@
1
- module CalculateAll
2
- module Helpers
3
- module_function
4
-
5
- # Convert aliases like :count to SQL aggregate functions like 'COUNT(*)'
6
- def decode_function_aliases(aliases)
7
- aliases.map do |key|
8
- function =
9
- case key
10
- when String
11
- key
12
- when :count
13
- "COUNT(*)"
14
- when /^(.*)_distinct_count$/, /^count_distinct_(.*)$/
15
- "COUNT(DISTINCT #{$1})"
16
- when /^(.*)_(count|sum|max|min|avg)$/
17
- "#{$2.upcase}(#{$1})"
18
- when /^(count|sum|max|min|avg)_(.*)$$/
19
- "#{$1.upcase}(#{$2})"
20
- when /^(.*)_average$/, /^average_(.*)$/
21
- "AVG(#{$1})"
22
- when /^(.*)_maximum$/, /^maximum_(.*)$/
23
- "MAX(#{$1})"
24
- when /^(.*)_minimum$/, /^minimum_(.*)$/
25
- "MIN(#{$1})"
26
- else
27
- raise ArgumentError, "Can't recognize function alias #{key}"
28
- end
29
- [key, function]
30
- end.to_h
31
- end
32
- end
33
- end
@@ -1,8 +0,0 @@
1
- module CalculateAll
2
- module Querying
3
- # @see CalculateAll#calculate_all
4
- def calculate_all(*args, **kwargs, &block)
5
- all.calculate_all(*args, **kwargs, &block)
6
- end
7
- end
8
- end