calculate-all 0.2.1 → 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 +5 -5
- data/.github/workflows/build.yml +41 -0
- data/.gitignore +2 -0
- data/CHANGELOG.md +20 -0
- data/Gemfile +13 -4
- data/README.md +112 -66
- data/Rakefile +19 -7
- data/bin/setup +5 -2
- data/calculate-all.gemspec +15 -21
- data/gemfiles/activerecord42.gemfile +12 -0
- data/gemfiles/activerecord50.gemfile +12 -0
- data/gemfiles/activerecord51.gemfile +12 -0
- data/gemfiles/activerecord52.gemfile +12 -0
- data/gemfiles/activerecord60.gemfile +12 -0
- data/gemfiles/activerecord61.gemfile +12 -0
- data/gemfiles/activerecord70.gemfile +12 -0
- data/lib/calculate-all/version.rb +1 -1
- data/lib/calculate-all.rb +101 -65
- metadata +16 -100
- data/.travis.yml +0 -4
- data/bin/console +0 -10
- data/lib/calculate-all/helpers.rb +0 -32
- data/lib/calculate-all/querying.rb +0 -5
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
|
-
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
2
|
+
SHA256:
|
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
|
@@ -0,0 +1,41 @@
|
|
1
|
+
name: build
|
2
|
+
on: [push, pull_request]
|
3
|
+
jobs:
|
4
|
+
build:
|
5
|
+
strategy:
|
6
|
+
fail-fast: false
|
7
|
+
matrix:
|
8
|
+
include:
|
9
|
+
- ruby: "3.4"
|
10
|
+
gemfile: Gemfile
|
11
|
+
- ruby: "3.1"
|
12
|
+
gemfile: gemfiles/activerecord70.gemfile
|
13
|
+
- ruby: "3.0"
|
14
|
+
gemfile: gemfiles/activerecord61.gemfile
|
15
|
+
- ruby: "2.7"
|
16
|
+
gemfile: gemfiles/activerecord60.gemfile
|
17
|
+
- ruby: "2.6"
|
18
|
+
gemfile: gemfiles/activerecord52.gemfile
|
19
|
+
- ruby: "2.6"
|
20
|
+
gemfile: gemfiles/activerecord51.gemfile
|
21
|
+
- ruby: "2.6"
|
22
|
+
gemfile: gemfiles/activerecord50.gemfile
|
23
|
+
- ruby: "2.3"
|
24
|
+
gemfile: gemfiles/activerecord42.gemfile
|
25
|
+
runs-on: ubuntu-latest
|
26
|
+
env:
|
27
|
+
BUNDLE_GEMFILE: ${{ matrix.gemfile }}
|
28
|
+
steps:
|
29
|
+
- uses: actions/checkout@v2
|
30
|
+
- uses: ruby/setup-ruby@v1
|
31
|
+
with:
|
32
|
+
ruby-version: ${{ matrix.ruby }}
|
33
|
+
bundler-cache: true
|
34
|
+
- uses: ankane/setup-postgres@v1
|
35
|
+
with:
|
36
|
+
database: calculate_all_test
|
37
|
+
- uses: ankane/setup-mysql@v1
|
38
|
+
with:
|
39
|
+
database: calculate_all_test
|
40
|
+
- run: mysql_tzinfo_to_sql /usr/share/zoneinfo | mysql -u root mysql
|
41
|
+
- run: bundle exec rake test
|
data/CHANGELOG.md
CHANGED
@@ -1,3 +1,23 @@
|
|
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
|
+
|
8
|
+
## 0.2.2
|
9
|
+
|
10
|
+
* Added support for Groupdate 4+ (Andrew <acekane1@gmail.com>)
|
11
|
+
* Tested with sqlite3, ruby 3.1, Rails 7
|
12
|
+
|
13
|
+
## 0.2.1
|
14
|
+
|
15
|
+
* Silence deprecation warnings (Forrest Ye <fye@mutan.io>)
|
16
|
+
|
17
|
+
## 0.2.0
|
18
|
+
|
19
|
+
* Rails 5 compatibility (Stef Schenkelaars <stef.schenkelaars@gmail.com>)
|
20
|
+
|
1
21
|
## 0.1.1
|
2
22
|
|
3
23
|
* groupdate compatibility
|
data/Gemfile
CHANGED
@@ -1,7 +1,16 @@
|
|
1
|
-
source
|
1
|
+
source "https://rubygems.org"
|
2
2
|
|
3
|
-
# Specify your gem's dependencies in calculate-all.gemspec
|
4
3
|
gemspec
|
5
4
|
|
6
|
-
gem
|
7
|
-
gem
|
5
|
+
gem "rake"
|
6
|
+
gem "minitest"
|
7
|
+
gem "minitest-reporters"
|
8
|
+
gem "activerecord", "~> 8.0.0"
|
9
|
+
gem "groupdate", "~> 6"
|
10
|
+
gem "pg"
|
11
|
+
gem "mysql2"
|
12
|
+
gem "sqlite3", ">= 2.1"
|
13
|
+
|
14
|
+
gem "ostruct"
|
15
|
+
|
16
|
+
gem "standardrb", require: false
|
data/README.md
CHANGED
@@ -1,36 +1,48 @@
|
|
1
1
|
# CalculateAll
|
2
2
|
|
3
|
-
Provides `#calculate_all` method
|
4
|
-
It's a
|
5
|
-
|
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`, `#sum`
|
5
|
+
and `#calculate`.
|
6
|
+
It allows you to fetch all of the above, as well as other aggregate function results,
|
7
|
+
in a single request, with support for grouping.
|
6
8
|
|
7
|
-
|
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.
|
8
12
|
|
9
13
|
## Usage
|
10
14
|
|
15
|
+
(example SQL snippets are given for PostgreSQL)
|
16
|
+
|
11
17
|
```ruby
|
12
|
-
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,
|
13
20
|
:count,
|
14
|
-
:count_distinct_user_id,
|
15
21
|
:price_max,
|
16
22
|
:price_min,
|
17
23
|
:price_avg,
|
18
|
-
|
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')"
|
19
28
|
)
|
20
|
-
#
|
21
|
-
#
|
22
|
-
#
|
23
|
-
#
|
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
|
24
33
|
# => {
|
25
|
-
# [1, "
|
34
|
+
# [1, "card"] => {
|
35
|
+
# payment_method: "card",
|
26
36
|
# count: 10,
|
27
|
-
# count_distinct_user_id: 5,
|
28
37
|
# price_max: 500,
|
29
38
|
# price_min: 100,
|
30
|
-
# price_avg:
|
31
|
-
#
|
39
|
+
# price_avg: 0.3e3,
|
40
|
+
# total_users: 5,
|
41
|
+
# price_median: 0.4e3,
|
42
|
+
# plan_ids: [4, 7, 12],
|
43
|
+
# earnings: 2340
|
32
44
|
# },
|
33
|
-
# [1, "
|
45
|
+
# [1, "cash"] => {
|
34
46
|
# ...
|
35
47
|
# }
|
36
48
|
# }
|
@@ -38,39 +50,37 @@ stats = Order.group(:department_id).group(:payment_method).calculate_all(
|
|
38
50
|
|
39
51
|
## Rationale
|
40
52
|
|
41
|
-
Active Record
|
42
|
-
But there's a whole world of
|
43
|
-
[
|
44
|
-
|
45
|
-
[
|
53
|
+
Active Record makes it really easy to use most common database aggregate functions like COUNT(), MAX(), MIN(), AVG(), SUM().
|
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.
|
46
59
|
|
47
|
-
Also, in many cases you
|
48
|
-
|
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.
|
61
|
+
However, it can calculate all of them in a single scan and a single request.
|
49
62
|
|
50
63
|
`#calculate_all` to the rescue!
|
51
64
|
|
52
65
|
## Arguments
|
53
66
|
|
54
|
-
`#calculate_all` accepts a
|
55
|
-
It could be either one string of SQL,
|
67
|
+
`#calculate_all` accepts a single SQL expression with aggregate functions,
|
56
68
|
|
57
69
|
```ruby
|
58
|
-
Model.calculate_all('SUM(price) / COUNT(DISTINCT user_id)')
|
70
|
+
Model.calculate_all('CAST(SUM(price) as decimal) / COUNT(DISTINCT user_id)')
|
59
71
|
```
|
60
72
|
|
61
|
-
|
73
|
+
or arbitrary symbols and keyword arguments with SQL snippets, aggregate function shortcuts or previously given grouping values.
|
62
74
|
|
63
75
|
```ruby
|
64
|
-
Model.
|
76
|
+
Model.group(:currency).calculate_all(
|
77
|
+
:average_price, :currency, total: :sum_price, average_spendings: 'SUM(price)::decimal / COUNT(DISTINCT user_id)'
|
78
|
+
)
|
65
79
|
```
|
66
|
-
or a list of one or more symbols without expressions, in which case `#calculate_all` tries to guess
|
67
|
-
what you wanted from it.
|
68
80
|
|
69
|
-
|
70
|
-
Model.calculate_all(:count, :average_price, :sum_price)
|
71
|
-
```
|
81
|
+
For convenience, `calculate_all(:count, :avg_column)` is the same as `caculate(count: :count, avg_column: :avg_column)`
|
72
82
|
|
73
|
-
|
83
|
+
Here's a cheatsheet of recognized shortcuts:
|
74
84
|
|
75
85
|
| symbol | would fetch
|
76
86
|
|------------------------------------------------------------------------|------------
|
@@ -82,28 +92,30 @@ It's not so smart right now, but here's a cheatsheet:
|
|
82
92
|
| `:avg_column1`, `:column1_avg`, `:average_column1`, `:column1_average` | `AVG(column1)`
|
83
93
|
| `:sum_column1`, `:column1_sum` | `SUM(column1)`
|
84
94
|
|
95
|
+
Other functions are a bit too database specific, and are better to be given with an explicit SQL snippet.
|
96
|
+
|
97
|
+
Please don't put values from unverified sources (like HTML form or javascript call) into expression list,
|
98
|
+
it could result in malicious SQL injection.
|
99
|
+
|
85
100
|
## Result
|
86
101
|
|
87
102
|
`#calculate_all` tries to mimic magic of Active Record's `#group`, `#count` and `#pluck`
|
88
103
|
so result type depends on arguments and on groupings.
|
89
104
|
|
90
|
-
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.
|
91
106
|
|
92
107
|
```ruby
|
93
|
-
|
94
|
-
#
|
95
|
-
Order.calculate_all('COUNT(DISTINCT user_id)')
|
96
|
-
# => 50
|
108
|
+
Order.calculate_all(:price_sum)
|
109
|
+
# => {price_sum: 123500}
|
97
110
|
```
|
98
111
|
|
99
|
-
If you have
|
112
|
+
If you have a single `group()`, it will return a hash of results with simple keys.
|
100
113
|
|
101
114
|
```ruby
|
102
|
-
# again, Order.group(:department_id).distinct.count(:user_id) would do the same
|
103
115
|
Order.group(:department_id).calculate_all(:count_distinct_user_id)
|
104
116
|
# => {
|
105
|
-
# 1 => 20,
|
106
|
-
# 2 => 10,
|
117
|
+
# 1 => {count_distinct_user_id: 20},
|
118
|
+
# 2 => {count_distinct_user_id: 10},
|
107
119
|
# ...
|
108
120
|
# }
|
109
121
|
```
|
@@ -111,31 +123,42 @@ Order.group(:department_id).calculate_all(:count_distinct_user_id)
|
|
111
123
|
If you have two or more groupings, each result will have an array as a key.
|
112
124
|
|
113
125
|
```ruby
|
114
|
-
Order.group(:department_id).group(:
|
126
|
+
Order.group(:department_id).group(:payment_method).calculate_all(:count)
|
115
127
|
# => {
|
116
|
-
# [1, "cash"] => 5,
|
117
|
-
# [1, "card"] => 15,
|
118
|
-
# [2, "cash"] => 1,
|
128
|
+
# [1, "cash"] => {count: 5},
|
129
|
+
# [1, "card"] => {count: 15},
|
130
|
+
# [2, "cash"] => {count: 1},
|
119
131
|
# ...
|
120
132
|
# }
|
121
133
|
```
|
122
134
|
|
123
|
-
If you provide
|
124
|
-
|
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
|
+
```
|
125
146
|
|
126
|
-
|
147
|
+
Otherwise, the results will be returned as hash(es) with symbol keys.
|
127
148
|
|
128
149
|
```ruby
|
129
|
-
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
|
+
)
|
130
153
|
# => {
|
131
|
-
# [1, 'cash'] => {min_price: 100, expr1: 5},
|
132
|
-
# [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},
|
133
156
|
# ...
|
134
157
|
# }
|
135
158
|
```
|
136
159
|
|
137
|
-
You can pass block to calculate_all
|
138
|
-
row in result hash (or returned as
|
160
|
+
You can pass a block to `calculate_all`. Rows will be passed to it, and returned value will be used instead of
|
161
|
+
the row in the result hash (or returned as-is if there's no grouping).
|
139
162
|
|
140
163
|
```ruby
|
141
164
|
Order.group(:country_id).calculate_all(:count, :avg_price) { |count:, avg_price:|
|
@@ -146,7 +169,7 @@ Order.group(:country_id).calculate_all(:count, :avg_price) { |count:, avg_price:
|
|
146
169
|
# 2 => "10 orders, 200 dollars average"
|
147
170
|
# }
|
148
171
|
|
149
|
-
Order.group(:country_id).calculate_all(
|
172
|
+
Order.group(:country_id).calculate_all("AVG(price)") { |avg_price| avg_price.to_i }
|
150
173
|
# => {
|
151
174
|
# 1 => 120,
|
152
175
|
# 2 => 200
|
@@ -154,6 +177,17 @@ Order.group(:country_id).calculate_all(:avg_price) { |avg_price| avg_price.to_i
|
|
154
177
|
|
155
178
|
Order.calculate_all(:count, :max_price, &OpenStruct.method(:new))
|
156
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
|
+
# }
|
157
191
|
```
|
158
192
|
|
159
193
|
## groupdate compatibility
|
@@ -161,16 +195,18 @@ Order.calculate_all(:count, :max_price, &OpenStruct.method(:new))
|
|
161
195
|
calculate-all should work with [groupdate](https://github.com/ankane/groupdate) too:
|
162
196
|
|
163
197
|
```ruby
|
164
|
-
Order.group_by_year(:created_at, last: 5
|
165
|
-
=> {
|
166
|
-
|
167
|
-
|
168
|
-
|
169
|
-
|
170
|
-
|
171
|
-
}
|
198
|
+
Order.group_by_year(:created_at, last: 5).calculate_all(:price_min, :price_max)
|
199
|
+
# => {
|
200
|
+
# Sun, 01 Jan 2012 => {},
|
201
|
+
# Tue, 01 Jan 2013 => {},
|
202
|
+
# Wed, 01 Jan 2014 => {},
|
203
|
+
# Thu, 01 Jan 2015 => {},
|
204
|
+
# Fri, 01 Jan 2016 => {:price_min=>100, :price_max=>500}
|
205
|
+
# }
|
172
206
|
```
|
173
207
|
|
208
|
+
It works even with groupdate < 4, though you'd have to explicitly provide `default_value: {}` for blank periods.
|
209
|
+
|
174
210
|
## Installation
|
175
211
|
|
176
212
|
Add this line to your application's Gemfile:
|
@@ -189,9 +225,19 @@ Or install it yourself as:
|
|
189
225
|
|
190
226
|
## Development
|
191
227
|
|
192
|
-
After checking out the repo, run `bin/setup` to install dependencies. Then, run `rake test` to run the tests.
|
228
|
+
After checking out the repo, run `bin/setup` to install dependencies. Then, run `rake test` to run the tests.
|
229
|
+
Run `BUNDLE_GEMFILE=gemfiles/activerecord60.gemfile bundle` then `BUNDLE_GEMFILE=gemfiles/activerecord60.gemfile rake`
|
230
|
+
to test agains 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
|
+
```
|
193
237
|
|
194
|
-
To install this gem onto your local machine, run `bundle exec rake install`. To release a new version, update the version
|
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).
|
195
241
|
|
196
242
|
## Contributing
|
197
243
|
|
data/Rakefile
CHANGED
@@ -1,10 +1,22 @@
|
|
1
|
-
require
|
2
|
-
require
|
1
|
+
require "bundler/gem_tasks"
|
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
|
|
10
|
-
|
19
|
+
desc "Run all adapter tests"
|
20
|
+
task test: ADAPTERS.map { |adapter| "test:#{adapter}" }
|
21
|
+
|
22
|
+
task default: :test
|
data/bin/setup
CHANGED
@@ -2,6 +2,9 @@
|
|
2
2
|
set -euo pipefail
|
3
3
|
IFS=$'\n\t'
|
4
4
|
|
5
|
+
echo "Installing gems..."
|
5
6
|
bundle install
|
6
|
-
|
7
|
-
|
7
|
+
echo "Creating postgres database..."
|
8
|
+
createdb calculate_all_test || echo "...failed"
|
9
|
+
echo "Creating mysql database..."
|
10
|
+
mysqladmin -u root create calculate_all_test || echo "...failed"
|
data/calculate-all.gemspec
CHANGED
@@ -1,30 +1,24 @@
|
|
1
|
-
|
2
|
-
lib = File.expand_path('../lib', __FILE__)
|
1
|
+
lib = File.expand_path("../lib", __FILE__)
|
3
2
|
$LOAD_PATH.unshift(lib) unless $LOAD_PATH.include?(lib)
|
4
|
-
require
|
3
|
+
require "calculate-all/version"
|
5
4
|
|
6
5
|
Gem::Specification.new do |spec|
|
7
|
-
spec.name
|
8
|
-
spec.version
|
9
|
-
spec.authors
|
10
|
-
spec.email
|
6
|
+
spec.name = "calculate-all"
|
7
|
+
spec.version = CalculateAll::VERSION
|
8
|
+
spec.authors = ["Alexey Trofimenko"]
|
9
|
+
spec.email = ["aronaxis@gmail.com"]
|
11
10
|
|
12
|
-
spec.summary
|
13
|
-
spec.description
|
14
|
-
spec.homepage
|
15
|
-
spec.license
|
11
|
+
spec.summary = "Fetch from database results of several aggregate functions at once"
|
12
|
+
spec.description = "Extends Active Record with #calculate_all method"
|
13
|
+
spec.homepage = "http://github.com/codesnik/calculate-all"
|
14
|
+
spec.license = "MIT"
|
16
15
|
|
17
|
-
spec.files
|
18
|
-
spec.bindir
|
19
|
-
spec.executables
|
16
|
+
spec.files = `git ls-files -z`.split("\x0").reject { |f| f.match(%r{^(test|spec|features)/}) }
|
17
|
+
spec.bindir = "exe"
|
18
|
+
spec.executables = spec.files.grep(%r{^exe/}) { |f| File.basename(f) }
|
20
19
|
spec.require_paths = ["lib"]
|
21
20
|
|
22
|
-
spec.
|
21
|
+
spec.required_ruby_version = '>= 2.3.0'
|
23
22
|
|
24
|
-
spec.
|
25
|
-
spec.add_development_dependency "rake", "~> 10.0"
|
26
|
-
spec.add_development_dependency "minitest"
|
27
|
-
spec.add_development_dependency "pg"
|
28
|
-
spec.add_development_dependency "mysql2"
|
29
|
-
spec.add_development_dependency "groupdate"
|
23
|
+
spec.add_dependency "activesupport", ">= 4.0.0"
|
30
24
|
end
|
data/lib/calculate-all.rb
CHANGED
@@ -1,89 +1,125 @@
|
|
1
|
-
require
|
2
|
-
require
|
3
|
-
require 'calculate-all/querying'
|
1
|
+
require "active_support"
|
2
|
+
require "calculate-all/version"
|
4
3
|
|
5
4
|
module CalculateAll
|
6
|
-
#
|
7
|
-
def calculate_all(*
|
8
|
-
|
9
|
-
#
|
10
|
-
|
11
|
-
|
5
|
+
# Calculates multiple aggregate values on a scope in one request, similarly to #calculate
|
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
|
+
# return row(s) directly without wrapping in Hash
|
9
|
+
if expression_shortcuts.size == 1 && expression_shortcuts.first.is_a?(String) &&
|
10
|
+
named_expressions.size == 0
|
12
11
|
return_plain_values = true
|
13
12
|
end
|
14
13
|
|
15
|
-
|
16
|
-
functions.merge!(
|
17
|
-
CalculateAll::Helpers.decode_function_aliases(function_aliases)
|
18
|
-
)
|
14
|
+
named_expressions = expression_shortcuts.map { |name| [name, name] }.to_h.merge(named_expressions)
|
19
15
|
|
20
|
-
|
21
|
-
|
22
|
-
raise ArgumentError, 'provide at least one function to calculate'
|
16
|
+
named_expressions.transform_values! do |shortcut|
|
17
|
+
Helpers.decode_expression_shortcut(shortcut, group_values)
|
23
18
|
end
|
24
19
|
|
25
|
-
|
26
|
-
# an array but it is an array with the final results instead of each group
|
27
|
-
# The plain_rows boolean states how the results should be used
|
28
|
-
if functions.size == 1 && group_values.size == 0
|
29
|
-
plain_rows = true
|
30
|
-
end
|
20
|
+
raise ArgumentError, "provide at least one expression to calculate" if named_expressions.empty?
|
31
21
|
|
32
|
-
#
|
33
|
-
|
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 }
|
34
27
|
|
35
|
-
|
36
|
-
|
37
|
-
|
38
|
-
|
39
|
-
|
40
|
-
|
41
|
-
|
42
|
-
|
43
|
-
|
44
|
-
|
45
|
-
|
46
|
-
# If no grouping, make sure it is still a results array
|
47
|
-
row = [row] if plain_rows
|
48
|
-
|
49
|
-
# If only one value, return a single value, else return a hash
|
50
|
-
if return_plain_values
|
51
|
-
value = row.last
|
28
|
+
results = {}
|
29
|
+
pluck(*columns).each do |row|
|
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
|
33
|
+
|
34
|
+
key = if group_values.size == 0
|
35
|
+
:ALL
|
36
|
+
elsif group_values.size == 1
|
37
|
+
# If only one group is provided, the resulting key is just a scalar value
|
38
|
+
row.first
|
52
39
|
else
|
53
|
-
|
40
|
+
# if multiple groups, the key will be an array.
|
41
|
+
row.first(group_values.size)
|
54
42
|
end
|
55
43
|
|
56
|
-
|
57
|
-
value = block.call(value) if block
|
44
|
+
value = value_mapping.transform_values { |index| row[index] }
|
58
45
|
|
59
|
-
|
60
|
-
return value if group_values.empty?
|
46
|
+
value = value.values.last if return_plain_values
|
61
47
|
|
62
|
-
|
63
|
-
|
64
|
-
|
65
|
-
|
66
|
-
|
67
|
-
|
48
|
+
results[key] = value
|
49
|
+
end
|
50
|
+
|
51
|
+
# Additional groupdate magic of filling empty periods with defaults
|
52
|
+
if defined?(Groupdate.process_result)
|
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.
|
55
|
+
default_value = return_plain_values ? nil : {}.freeze
|
56
|
+
results = Groupdate.process_result(self, results, default_value: default_value)
|
57
|
+
end
|
58
|
+
|
59
|
+
if block
|
60
|
+
results.transform_values! do |value|
|
61
|
+
return_plain_values ? block.call(value) : block.call(**value)
|
68
62
|
end
|
63
|
+
end
|
69
64
|
|
70
|
-
|
71
|
-
|
65
|
+
# Return unwrapped hash directly for scope without any .group()
|
66
|
+
if group_values.empty?
|
67
|
+
results[:ALL]
|
68
|
+
else
|
69
|
+
results
|
70
|
+
end
|
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
|
72
102
|
end
|
103
|
+
end
|
73
104
|
|
74
|
-
|
75
|
-
|
105
|
+
module Querying
|
106
|
+
# @see CalculateAll#calculate_all
|
107
|
+
def calculate_all(*args, **kwargs, &block)
|
108
|
+
all.calculate_all(*args, **kwargs, &block)
|
109
|
+
end
|
76
110
|
end
|
77
111
|
end
|
78
112
|
|
79
|
-
|
80
|
-
ActiveRecord::
|
113
|
+
ActiveSupport.on_load(:active_record) do
|
114
|
+
# Make the calculate_all method available for all ActiveRecord::Relations instances
|
115
|
+
ActiveRecord::Relation.include CalculateAll
|
81
116
|
|
82
|
-
# Make the calculate_all method available for all ActiveRecord::Base classes
|
83
|
-
# You can for example call Orders.calculate_all(:count, :sum_cents)
|
84
|
-
ActiveRecord::Base.extend CalculateAll::Querying
|
117
|
+
# Make the calculate_all method available for all ActiveRecord::Base classes
|
118
|
+
# You can for example call Orders.calculate_all(:count, :sum_cents)
|
119
|
+
ActiveRecord::Base.extend CalculateAll::Querying
|
85
120
|
|
86
|
-
# A hack for groupdate since it checks if the calculate_all method is defined
|
87
|
-
# on the ActiveRecord::Calculations module. It is never called but it is just
|
88
|
-
# needed for the check.
|
89
|
-
ActiveRecord::Calculations.include CalculateAll::Querying
|
121
|
+
# A hack for groupdate 3.0 since it checks if the calculate_all method is defined
|
122
|
+
# on the ActiveRecord::Calculations module. It is never called but it is just
|
123
|
+
# needed for the check.
|
124
|
+
ActiveRecord::Calculations.include CalculateAll::Querying
|
125
|
+
end
|
metadata
CHANGED
@@ -1,113 +1,28 @@
|
|
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
|
-
-
|
8
|
-
autorequire:
|
7
|
+
- Alexey Trofimenko
|
9
8
|
bindir: exe
|
10
9
|
cert_chain: []
|
11
|
-
date:
|
10
|
+
date: 2025-05-18 00:00:00.000000000 Z
|
12
11
|
dependencies:
|
13
12
|
- !ruby/object:Gem::Dependency
|
14
|
-
name:
|
13
|
+
name: activesupport
|
15
14
|
requirement: !ruby/object:Gem::Requirement
|
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:
|
27
|
-
- !ruby/object:Gem::Dependency
|
28
|
-
name: bundler
|
29
|
-
requirement: !ruby/object:Gem::Requirement
|
30
|
-
requirements:
|
31
|
-
- - "~>"
|
32
|
-
- !ruby/object:Gem::Version
|
33
|
-
version: '1.10'
|
34
|
-
type: :development
|
35
|
-
prerelease: false
|
36
|
-
version_requirements: !ruby/object:Gem::Requirement
|
37
|
-
requirements:
|
38
|
-
- - "~>"
|
39
|
-
- !ruby/object:Gem::Version
|
40
|
-
version: '1.10'
|
41
|
-
- !ruby/object:Gem::Dependency
|
42
|
-
name: rake
|
43
|
-
requirement: !ruby/object:Gem::Requirement
|
44
|
-
requirements:
|
45
|
-
- - "~>"
|
46
|
-
- !ruby/object:Gem::Version
|
47
|
-
version: '10.0'
|
48
|
-
type: :development
|
49
|
-
prerelease: false
|
50
|
-
version_requirements: !ruby/object:Gem::Requirement
|
51
|
-
requirements:
|
52
|
-
- - "~>"
|
53
|
-
- !ruby/object:Gem::Version
|
54
|
-
version: '10.0'
|
55
|
-
- !ruby/object:Gem::Dependency
|
56
|
-
name: minitest
|
57
|
-
requirement: !ruby/object:Gem::Requirement
|
58
|
-
requirements:
|
59
|
-
- - ">="
|
60
|
-
- !ruby/object:Gem::Version
|
61
|
-
version: '0'
|
62
|
-
type: :development
|
63
|
-
prerelease: false
|
64
|
-
version_requirements: !ruby/object:Gem::Requirement
|
65
|
-
requirements:
|
66
|
-
- - ">="
|
67
|
-
- !ruby/object:Gem::Version
|
68
|
-
version: '0'
|
69
|
-
- !ruby/object:Gem::Dependency
|
70
|
-
name: pg
|
71
|
-
requirement: !ruby/object:Gem::Requirement
|
72
|
-
requirements:
|
73
|
-
- - ">="
|
74
|
-
- !ruby/object:Gem::Version
|
75
|
-
version: '0'
|
76
|
-
type: :development
|
77
|
-
prerelease: false
|
78
|
-
version_requirements: !ruby/object:Gem::Requirement
|
79
|
-
requirements:
|
80
|
-
- - ">="
|
81
|
-
- !ruby/object:Gem::Version
|
82
|
-
version: '0'
|
83
|
-
- !ruby/object:Gem::Dependency
|
84
|
-
name: mysql2
|
85
|
-
requirement: !ruby/object:Gem::Requirement
|
86
|
-
requirements:
|
87
|
-
- - ">="
|
88
|
-
- !ruby/object:Gem::Version
|
89
|
-
version: '0'
|
90
|
-
type: :development
|
91
|
-
prerelease: false
|
92
|
-
version_requirements: !ruby/object:Gem::Requirement
|
93
|
-
requirements:
|
94
|
-
- - ">="
|
95
|
-
- !ruby/object:Gem::Version
|
96
|
-
version: '0'
|
97
|
-
- !ruby/object:Gem::Dependency
|
98
|
-
name: groupdate
|
99
|
-
requirement: !ruby/object:Gem::Requirement
|
100
|
-
requirements:
|
101
|
-
- - ">="
|
102
|
-
- !ruby/object:Gem::Version
|
103
|
-
version: '0'
|
104
|
-
type: :development
|
105
|
-
prerelease: false
|
106
|
-
version_requirements: !ruby/object:Gem::Requirement
|
107
|
-
requirements:
|
108
|
-
- - ">="
|
109
|
-
- !ruby/object:Gem::Version
|
110
|
-
version: '0'
|
25
|
+
version: 4.0.0
|
111
26
|
description: 'Extends Active Record with #calculate_all method'
|
112
27
|
email:
|
113
28
|
- aronaxis@gmail.com
|
@@ -115,26 +30,29 @@ executables: []
|
|
115
30
|
extensions: []
|
116
31
|
extra_rdoc_files: []
|
117
32
|
files:
|
33
|
+
- ".github/workflows/build.yml"
|
118
34
|
- ".gitignore"
|
119
|
-
- ".travis.yml"
|
120
35
|
- CHANGELOG.md
|
121
36
|
- Gemfile
|
122
37
|
- LICENSE.txt
|
123
38
|
- README.md
|
124
39
|
- Rakefile
|
125
40
|
- _config.yml
|
126
|
-
- bin/console
|
127
41
|
- bin/setup
|
128
42
|
- calculate-all.gemspec
|
43
|
+
- gemfiles/activerecord42.gemfile
|
44
|
+
- gemfiles/activerecord50.gemfile
|
45
|
+
- gemfiles/activerecord51.gemfile
|
46
|
+
- gemfiles/activerecord52.gemfile
|
47
|
+
- gemfiles/activerecord60.gemfile
|
48
|
+
- gemfiles/activerecord61.gemfile
|
49
|
+
- gemfiles/activerecord70.gemfile
|
129
50
|
- lib/calculate-all.rb
|
130
|
-
- lib/calculate-all/helpers.rb
|
131
|
-
- lib/calculate-all/querying.rb
|
132
51
|
- lib/calculate-all/version.rb
|
133
52
|
homepage: http://github.com/codesnik/calculate-all
|
134
53
|
licenses:
|
135
54
|
- MIT
|
136
55
|
metadata: {}
|
137
|
-
post_install_message:
|
138
56
|
rdoc_options: []
|
139
57
|
require_paths:
|
140
58
|
- lib
|
@@ -142,16 +60,14 @@ required_ruby_version: !ruby/object:Gem::Requirement
|
|
142
60
|
requirements:
|
143
61
|
- - ">="
|
144
62
|
- !ruby/object:Gem::Version
|
145
|
-
version:
|
63
|
+
version: 2.3.0
|
146
64
|
required_rubygems_version: !ruby/object:Gem::Requirement
|
147
65
|
requirements:
|
148
66
|
- - ">="
|
149
67
|
- !ruby/object:Gem::Version
|
150
68
|
version: '0'
|
151
69
|
requirements: []
|
152
|
-
|
153
|
-
rubygems_version: 2.4.5.1
|
154
|
-
signing_key:
|
70
|
+
rubygems_version: 3.6.2
|
155
71
|
specification_version: 4
|
156
72
|
summary: Fetch from database results of several aggregate functions at once
|
157
73
|
test_files: []
|
data/.travis.yml
DELETED
data/bin/console
DELETED
@@ -1,32 +0,0 @@
|
|
1
|
-
module CalculateAll
|
2
|
-
module Helpers
|
3
|
-
module_function
|
4
|
-
# Method to convert function aliases like :count to SQL commands like 'COUNT(*)'
|
5
|
-
def decode_function_aliases(aliases)
|
6
|
-
aliases.map do |key|
|
7
|
-
function =
|
8
|
-
case key
|
9
|
-
when String
|
10
|
-
key
|
11
|
-
when :count
|
12
|
-
'COUNT(*)'
|
13
|
-
when /^(.*)_distinct_count$/, /^count_distinct_(.*)$/
|
14
|
-
"COUNT(DISTINCT #{$1})"
|
15
|
-
when /^(.*)_(count|sum|max|min|avg)$/
|
16
|
-
"#{$2.upcase}(#{$1})"
|
17
|
-
when /^(count|sum|max|min|avg)_(.*)$$/
|
18
|
-
"#{$1.upcase}(#{$2})"
|
19
|
-
when /^(.*)_average$/, /^average_(.*)$/
|
20
|
-
"AVG(#{$1})"
|
21
|
-
when /^(.*)_maximum$/, /^maximum_(.*)$/
|
22
|
-
"MAX(#{$1})"
|
23
|
-
when /^(.*)_minimum$/, /^minimum_(.*)$/
|
24
|
-
"MIN(#{$1})"
|
25
|
-
else
|
26
|
-
raise ArgumentError, "Can't recognize function alias #{key}"
|
27
|
-
end
|
28
|
-
[key, function]
|
29
|
-
end.to_h
|
30
|
-
end
|
31
|
-
end
|
32
|
-
end
|