calculate-all 0.2.2 → 0.3.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.
- checksums.yaml +4 -4
- data/.github/workflows/build.yml +4 -2
- data/CHANGELOG.md +7 -0
- data/Gemfile +6 -3
- data/README.md +86 -49
- data/Rakefile +16 -4
- data/calculate-all.gemspec +3 -1
- data/gemfiles/activerecord42.gemfile +1 -0
- data/gemfiles/activerecord50.gemfile +1 -0
- data/gemfiles/activerecord51.gemfile +1 -0
- data/gemfiles/activerecord52.gemfile +1 -0
- data/gemfiles/activerecord60.gemfile +1 -0
- data/gemfiles/activerecord61.gemfile +1 -0
- data/gemfiles/activerecord70.gemfile +12 -0
- data/lib/calculate-all/version.rb +1 -1
- data/lib/calculate-all.rb +64 -27
- metadata +7 -11
- data/lib/calculate-all/helpers.rb +0 -33
- data/lib/calculate-all/querying.rb +0 -8
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA256:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: 0ccade1f3bc4b62c2842ef335e27d0c7a11a90fa18e90f3b1260bb572243a4b6
|
4
|
+
data.tar.gz: 9d8dd96e03c6155f828d5fb586a652e9f0d94a5cbb21cfb41fc3c5022772501c
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: df3d5a6c997473a188f824f865951c05ed8d054758906c6647d4c868a55a37cb55cc0c5981b0bbe4106214d386a03c11fcb41211498c58b7f022a779b265d192
|
7
|
+
data.tar.gz: ca3c3d75e899ff84f85886aeb649f86cc81f0612476297102036a7d991ed872819381c9505f7ed34b3713aed508960f71d8377ac986d92599f9bf585fe493d78
|
data/.github/workflows/build.yml
CHANGED
@@ -6,8 +6,10 @@ jobs:
|
|
6
6
|
fail-fast: false
|
7
7
|
matrix:
|
8
8
|
include:
|
9
|
-
- ruby: "3.
|
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.
|
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,10 @@
|
|
1
|
+
## 0.3.0
|
2
|
+
|
3
|
+
* Allow expression shortcuts as attribute values too for renaming
|
4
|
+
* Allow grouping expressions to be returned in rows too
|
5
|
+
* Breaking change: only single *string* expression argument is returning unwrapped rows now.
|
6
|
+
Single expression shortcut like `:count` will be expanded to `{count: value}` rows.
|
7
|
+
|
1
8
|
## 0.2.2
|
2
9
|
|
3
10
|
* Added support for Groupdate 4+ (Andrew <acekane1@gmail.com>)
|
data/Gemfile
CHANGED
@@ -4,10 +4,13 @@ gemspec
|
|
4
4
|
|
5
5
|
gem "rake"
|
6
6
|
gem "minitest"
|
7
|
-
gem "
|
8
|
-
gem "
|
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", "
|
12
|
+
gem "sqlite3", ">= 2.1"
|
13
|
+
|
14
|
+
gem "ostruct"
|
12
15
|
|
13
16
|
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
|
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
|
-
|
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
|
-
|
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
|
-
#
|
23
|
-
#
|
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, "
|
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
|
-
#
|
40
|
+
# total_users: 5,
|
41
|
+
# price_median: 0.4e3,
|
42
|
+
# plan_ids: [4, 7, 12],
|
43
|
+
# earnings: 2340
|
33
44
|
# },
|
34
|
-
# [1, "
|
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
|
46
|
-
|
47
|
-
MySQL
|
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
|
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
|
-
|
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.
|
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
|
-
|
72
|
-
Model.calculate_all(:count, :average_price, :sum_price)
|
73
|
-
```
|
81
|
+
For convenience, `calculate_all(:count, :avg_column)` is the same as `caculate(count: :count, avg_column: :avg_column)`
|
74
82
|
|
75
|
-
|
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
|
105
|
+
If you have no `group()` on underlying scope, `#calculate_all` will return just one row.
|
96
106
|
|
97
107
|
```ruby
|
98
|
-
|
99
|
-
#
|
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(:
|
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
|
-
|
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
|
-
|
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(
|
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,
|
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(
|
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_price800>
|
190
|
+
# }
|
162
191
|
```
|
163
192
|
|
164
193
|
## groupdate compatibility
|
@@ -200,7 +229,15 @@ After checking out the repo, run `bin/setup` to install dependencies. Then, run
|
|
200
229
|
Run `BUNDLE_GEMFILE=gemfiles/activerecord60.gemfile bundle` then `BUNDLE_GEMFILE=gemfiles/activerecord60.gemfile rake`
|
201
230
|
to test agains specific active record version.
|
202
231
|
|
203
|
-
To
|
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
|
+
```
|
237
|
+
|
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
|
-
|
5
|
-
|
6
|
-
|
7
|
-
|
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
|
data/calculate-all.gemspec
CHANGED
@@ -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.
|
21
|
+
spec.required_ruby_version = '>= 2.3.0'
|
22
|
+
|
23
|
+
spec.add_dependency "activesupport", ">= 4.0.0"
|
22
24
|
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(*
|
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
|
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
|
-
|
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
|
-
|
17
|
-
|
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
|
-
|
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
|
25
|
-
# it will return an array of
|
26
|
-
|
27
|
-
row = [row]
|
28
|
-
end
|
30
|
+
# If pluck called with with a single argument
|
31
|
+
# it will return an array of sclars 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.
|
38
|
+
row.first
|
35
39
|
else
|
36
40
|
# if multiple groups, the key will be an array.
|
37
|
-
row.
|
41
|
+
row.first(group_values.size)
|
38
42
|
end
|
39
43
|
|
40
|
-
value =
|
41
|
-
|
42
|
-
|
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
|
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 surprize 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 #{key}"
|
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.
|
4
|
+
version: 0.3.0
|
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-
|
10
|
+
date: 2025-05-18 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:
|
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:
|
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:
|
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.
|
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
|