activerecord-summarize 0.4.0 → 0.5.1

Sign up to get free protection for your applications and to get access to all the features.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 0e87406ae4f6c3aeec411824af98bb21b12457815f24e09f446ed8576bfc1352
4
- data.tar.gz: 266e9065f9e49e458fa427d3a82c55eae82eafd983c27a5c8729125c2c75eb57
3
+ metadata.gz: f469f566e328d4cc697d23295d95c477f6eb66c5d9246874b36bc59b5fbdcac0
4
+ data.tar.gz: 7a7315482f217384462c869796531e1ea27511ba1787f435aec1eed754601506
5
5
  SHA512:
6
- metadata.gz: b515f889c18886602e6a695982ad755bdbb44b38ec93a1422cc92fb0ae9da612e3a7f2854d12c07864bedde52134f121823da201a8f52383564643ca2a7b8e84
7
- data.tar.gz: e67437ac8503938c9f6f671fcf7c952aed1998efb045ff6a05a7d2a16e2c50ed7d79519f93b8a0d196cbf7d9d1bd9c23673e51487f7be8de3554cb27dde264fc
6
+ metadata.gz: 66a61050cec2736eed06d01b5889f85259e0625daa530376fb840f02cb93a041483f335bda4614d2925b9b4d1ae22f811d8fa5d005dad82d559bd1a5d64f0bb5
7
+ data.tar.gz: b3f3e98e768a019f4c2a32c06d61fb84a50fcbc9f6866df35c1c254ee7d8feb2fd300a286a33cefd6407d12f8e67353348e7e600290ccfb2ae192e9dffd9fd9b
data/CHANGELOG.md CHANGED
@@ -1,3 +1,13 @@
1
+ ## [0.5.1] - 2023-08-16
2
+
3
+ - **BUGFIX:** Starting with version 7.0.5, the behavior of ActiveRecord's `pluck` changed: when you pluck multiple values with the same aggregate function (e.g., `sum`), in PostgreSQL, the data type of the last such value is now applied to all such values, though they used to be inferred correctly. Our solution is to add an explicit alias to each result column.
4
+
5
+ ## [0.5.0] - 2023-05-14
6
+
7
+ - **FEATURE:** Your `summarize` blocks won't need to accept the proc second argument as often, because `ChainableResult` methods will also resolve their arguments. E.g., `query.summarize {|q| @mult = q.sum(:a) * q.sum(:b) }` now works, where previously you would have needed to write `query.summarize {|q,with| @mult = with[q.sum(:a),q.sum(:b)] {|a,b| a * b } }`.
8
+
9
+ - **IMPROVEMENT:** The conventional name of the proc provided as an optional second argument to `summarize` blocks is now `with_resolved` instead of `with`. Interactively teaching `activerecord-summarize` to some people showed that this was an improvement in clarity. The local name of the proc has always been under your control (it's your block!), so this doesn't affect anything besides documentation and tests, but if for some reason you accessed the proc at its internal name of `ChainableResult::WITH`, that will still work, too, even though we now refer to it as `ChainableResult::WITH_RESOLVED`.
10
+
1
11
  ## [0.4.0] - 2023-02-27
2
12
 
3
13
  - **FEATURE:** Support for top-level .group(:belongs_to_association), returning hash with models as keys.
data/Gemfile CHANGED
@@ -9,5 +9,6 @@ gem "rake", "~> 13.0"
9
9
  gem "minitest", "~> 5.0"
10
10
  gem "standard", "~> 1.3"
11
11
 
12
- gem "activerecord", "7.0.3"
13
- gem "sqlite3", "1.4.2"
12
+ gem "activerecord", "7.0.7"
13
+ gem "sqlite3", "1.6.3"
14
+ gem "pg", "~> 1.5"
data/Gemfile.lock CHANGED
@@ -1,68 +1,86 @@
1
1
  PATH
2
2
  remote: .
3
3
  specs:
4
- activerecord-summarize (0.4.0)
4
+ activerecord-summarize (0.5.1)
5
5
  activerecord (>= 5.0)
6
6
 
7
7
  GEM
8
8
  remote: https://rubygems.org/
9
9
  specs:
10
- activemodel (7.0.3)
11
- activesupport (= 7.0.3)
12
- activerecord (7.0.3)
13
- activemodel (= 7.0.3)
14
- activesupport (= 7.0.3)
15
- activesupport (7.0.3)
10
+ activemodel (7.0.7)
11
+ activesupport (= 7.0.7)
12
+ activerecord (7.0.7)
13
+ activemodel (= 7.0.7)
14
+ activesupport (= 7.0.7)
15
+ activesupport (7.0.7)
16
16
  concurrent-ruby (~> 1.0, >= 1.0.2)
17
17
  i18n (>= 1.6, < 2)
18
18
  minitest (>= 5.1)
19
19
  tzinfo (~> 2.0)
20
20
  ast (2.4.2)
21
- concurrent-ruby (1.1.10)
22
- i18n (1.10.0)
21
+ concurrent-ruby (1.2.2)
22
+ i18n (1.14.1)
23
23
  concurrent-ruby (~> 1.0)
24
- minitest (5.15.0)
25
- parallel (1.21.0)
26
- parser (3.1.1.0)
24
+ json (2.6.3)
25
+ language_server-protocol (3.17.0.3)
26
+ lint_roller (1.1.0)
27
+ minitest (5.19.0)
28
+ parallel (1.23.0)
29
+ parser (3.2.2.3)
27
30
  ast (~> 2.4.1)
31
+ racc
32
+ pg (1.5.3)
33
+ racc (1.7.1)
28
34
  rainbow (3.1.1)
29
35
  rake (13.0.6)
30
- regexp_parser (2.2.1)
31
- rexml (3.2.5)
32
- rubocop (1.25.1)
36
+ regexp_parser (2.8.1)
37
+ rexml (3.2.6)
38
+ rubocop (1.52.1)
39
+ json (~> 2.3)
33
40
  parallel (~> 1.10)
34
- parser (>= 3.1.0.0)
41
+ parser (>= 3.2.2.3)
35
42
  rainbow (>= 2.2.2, < 4.0)
36
43
  regexp_parser (>= 1.8, < 3.0)
37
- rexml
38
- rubocop-ast (>= 1.15.1, < 2.0)
44
+ rexml (>= 3.2.5, < 4.0)
45
+ rubocop-ast (>= 1.28.0, < 2.0)
39
46
  ruby-progressbar (~> 1.7)
40
- unicode-display_width (>= 1.4.0, < 3.0)
41
- rubocop-ast (1.16.0)
42
- parser (>= 3.1.1.0)
43
- rubocop-performance (1.13.2)
47
+ unicode-display_width (>= 2.4.0, < 3.0)
48
+ rubocop-ast (1.29.0)
49
+ parser (>= 3.2.1.0)
50
+ rubocop-performance (1.18.0)
44
51
  rubocop (>= 1.7.0, < 2.0)
45
52
  rubocop-ast (>= 0.4.0)
46
- ruby-progressbar (1.11.0)
47
- sqlite3 (1.4.2)
48
- standard (1.7.2)
49
- rubocop (= 1.25.1)
50
- rubocop-performance (= 1.13.2)
51
- tzinfo (2.0.4)
53
+ ruby-progressbar (1.13.0)
54
+ sqlite3 (1.6.3-arm64-darwin)
55
+ sqlite3 (1.6.3-x86_64-linux)
56
+ standard (1.30.1)
57
+ language_server-protocol (~> 3.17.0.2)
58
+ lint_roller (~> 1.0)
59
+ rubocop (~> 1.52.0)
60
+ standard-custom (~> 1.0.0)
61
+ standard-performance (~> 1.1.0)
62
+ standard-custom (1.0.2)
63
+ lint_roller (~> 1.0)
64
+ rubocop (~> 1.50)
65
+ standard-performance (1.1.2)
66
+ lint_roller (~> 1.1)
67
+ rubocop-performance (~> 1.18.0)
68
+ tzinfo (2.0.6)
52
69
  concurrent-ruby (~> 1.0)
53
- unicode-display_width (2.1.0)
70
+ unicode-display_width (2.4.2)
54
71
 
55
72
  PLATFORMS
56
73
  arm64-darwin-21
57
74
  x86_64-linux
58
75
 
59
76
  DEPENDENCIES
60
- activerecord (= 7.0.3)
77
+ activerecord (= 7.0.7)
61
78
  activerecord-summarize!
62
79
  minitest (~> 5.0)
80
+ pg (~> 1.5)
63
81
  rake (~> 13.0)
64
- sqlite3 (= 1.4.2)
82
+ sqlite3 (= 1.6.3)
65
83
  standard (~> 1.3)
66
84
 
67
85
  BUNDLED WITH
68
- 2.3.3
86
+ 2.4.13
data/README.md CHANGED
@@ -59,7 +59,7 @@ Purchase.complete.left_joins(:region).summarize do |purchases|
59
59
  end
60
60
  ```
61
61
 
62
- Until the `summarize` block ends, the return value of your calculations are `ChainableResult::Future` instances, a bit like a Promise with a more convenient API. You can call any method you like on a `ChainableResult`, and you'll get back another `ChainableResult`, and they'll all turn out alright in the end—provided you called methods that would have worked if you had run that calculation without `summarize`. OTOH, using a `ChainableResult` as an argument to another method generally will not work.
62
+ Until the `summarize` block ends, the return value of your calculations are `ChainableResult::Future` instances, a bit like a Promise with a more convenient API. You can call any method you like on a `ChainableResult`, and you'll get back another `ChainableResult`, and they'll all turn out alright in the end—provided you called methods that would have worked if you had run that calculation without `summarize`. OTOH, using a `ChainableResult` as an argument to a method of a non-`ChainableResult` generally will not work.
63
63
 
64
64
  ```ruby
65
65
  Purchase.last_quarter.complete.summarize do |purchases|
@@ -68,24 +68,28 @@ Purchase.last_quarter.complete.summarize do |purchases|
68
68
  @vc_projection = @sales * 3
69
69
  # And this won't:
70
70
  @vc_projection = 3 * @sales
71
+ # But this will work since v0.5.0...
72
+ @units_sold = purchases.sum(:units)
73
+ # ...because methods of `ChainableResult` now resolve their argument(s)
74
+ @avg_unit_price = @sales / @units_sold
71
75
  end
72
76
  ```
73
77
 
74
- If, within a `summarize` block, you want to combine data from more than one `ChainableResult`, you must use the otherwise-optional second argument yielded to the block, a `proc` I like to name `with`. Pass it all the results you want to combine and a block that combines them and returns the new result:
78
+ If, within a `summarize` block, you want to combine data from more than one `ChainableResult`, you may need to use the otherwise-optional second argument yielded to the block, a `proc` I like to name `with_resolved`. Pass it all the results you want to combine and a block that combines them and returns the new result:
75
79
 
76
80
  ```ruby
77
- Purchase.complete.left_joins(:promotion).summarize do |purchases, with|
81
+ Purchase.complete.left_joins(:promotion).summarize do |purchases, with_resolved|
78
82
  @all_revenue = purchases.sum(:amount)
79
83
  promotions = purchases.where.not(promotions: {id: nil})
80
84
  @promotion_sales = promotions.count
81
85
  @promotion_discounts = promotions.sum("promotions.discount_amount")
82
- @avg_discount = with[@promotion_sales, @promotion_discounts] do |sales, discounts|
86
+ @avg_discount = with_resolved[@promotion_sales, @promotion_discounts] do |sales, discounts|
83
87
  sales.zero? ? 0 : discounts / sales
84
88
  end
85
89
  end
86
90
  ```
87
91
 
88
- Treat a `with` block as a pure function: i.e., return the value you care about, and don't set or change any other state within the block. Behavior in any other case is undefined.
92
+ Treat a `with_resolved` block as a pure function: i.e., return the value you care about, and don't set or change any other state within the block. Behavior in any other case is undefined.
89
93
 
90
94
  ## Escape hatch
91
95
 
@@ -93,7 +97,7 @@ The query generated by `summarize` is often much faster than equivalent queries
93
97
 
94
98
  By design, every operation performed with `summarize` is correct and corresponds to normal `ActiveRecord` behavior, and any operations that can't be done correctly this way or aren't yet will raise exceptions. But only imperfect humans have worked on this gem, so you might also wonder if `summarize` is producing correct results.
95
99
 
96
- Fortunately, you can easily check both with `summarize(noop: true)`, which causes `summarize` to yield the original relation it was called on and a trivial `with` proc. The block will be executed as though `summarize` were not involved, with each calculation executing separately and immediately returning numbers or hashes.
100
+ Fortunately, you can easily check both with `summarize(noop: true)`, which causes `summarize` to yield the original relation it was called on and a trivial `with_resolved` proc. The block will be executed as though `summarize` were not involved, with each calculation executing separately and immediately returning numbers or hashes.
97
101
 
98
102
  If you do find any case where you get different results with `summarize(noop: true)`, I'd be grateful if you filed an issue.
99
103
 
@@ -117,10 +121,10 @@ When the parent relation already has `.group` applied, `pure: true` is implied a
117
121
  Build even more complex queries by using `summarize` on a relation that already has `.group` applied. Results are grouped just like a standard `.group(*expressions).count`, but instead of single numbers, the values are whatever set of calculations you return from the block, including further `.group(*more).calculate(:sum|:count,*args)` calculations, in whatever `Array` or `Hash` shape you arrange them. For example:
118
122
 
119
123
  ```ruby
120
- puts Purchase.last_year.complete.group(:region_id).summarize do |purchases,with|
124
+ puts Purchase.last_year.complete.group(:region_id).summarize do |purchases,with_resolved|
121
125
  total = purchases.count
122
126
  by_quarter = purchases.group(CREATED_TO_YEAR_SQL, CREATED_TO_QUARTER_SQL).count.sort.to_h
123
- target = with[total / 4, by_quarter.values.max] {|avg_q, best_q| [avg_q * 1.25, best_q].max.round }
127
+ target = with_resolved[total / 4, by_quarter.values.max] {|avg_q, best_q| [avg_q * 1.25, best_q].max.round }
124
128
  {last_year: total, quarters: by_quarter, unit_target: target}
125
129
  end
126
130
  # Output:
@@ -150,13 +154,13 @@ When the relation already has `group` applied, for correct results, `summarize`
150
154
 
151
155
  ```ruby
152
156
  # A trivial example:
153
- Purchase.complete.group(:region_id).summarize {|purchases| purchases.sum(:amount) }
157
+ Purchase.complete.group(:region).summarize {|purchases| purchases.sum(:amount) }
154
158
 
155
159
  # ...is exactly equivalent to:
156
- Purchase.complete.group(:region_id).sum(:amount)
160
+ Purchase.complete.group(:region).sum(:amount)
157
161
 
158
162
  # But if there were three regions, what should the value of @target be in this case?
159
- region_targets = Purchase.last_quarter.complete.group(:region_id).summarize do |purchases|
163
+ region_targets = Purchase.last_quarter.complete.group(:region).summarize do |purchases|
160
164
  @target = purchases.sum(:amount) * 1.25
161
165
  end
162
166
  ```
@@ -170,7 +174,9 @@ Instead the block is evaluated once to determine what calculations need to be ru
170
174
 
171
175
  ## Development
172
176
 
173
- After checking out the repo, run `bin/setup` to install dependencies. Then, run `rake test` to run the tests. You can also run `bin/console` for an interactive prompt that will allow you to experiment.
177
+ Run `bin/setup` to install dependencies. If you don't have PostgreSQL installed, comment out the `pg` line in `Gemfile` first. Then, run `bundle exec rake test` to run the tests. You can also run `bin/console` for an interactive prompt that will allow you to experiment.
178
+
179
+ Tests and `bin/console` support SQLite and PostgreSQL: (un)comment the appropriate lines at the top of `test/test_data.rb` to choose. In the future, we'll have a nicer solution. If you want to use PostgreSQL, run `CREATE DATABASE summarize_test;` as your default user.
174
180
 
175
181
  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 the created tag, and push the `.gem` file to [rubygems.org](https://rubygems.org).
176
182
 
@@ -69,19 +69,19 @@ def dashboard
69
69
  # If you forget, `daily_posts.popular.count` will raise `Unsummarizable` with a helpful message.
70
70
  all_posts = Post.where(subreddit: @subreddits.select(:id)).where(created_at: 30.days.ago..)
71
71
  .left_joins(:popularity_threshold_setting).order(:created_at)
72
- @subreddit_stats = all_posts.group(:subreddit_id).summarize do |posts, with|
72
+ @subreddit_stats = all_posts.group(:subreddit_id).summarize do |posts, with_resolved|
73
73
  daily_posts = posts.group("posts.created_at::date")
74
74
  dow_not_burried = posts.where(karma: 0..).group("EXTRACT(DOW FROM posts.created_at)")
75
75
  {
76
76
  posts_created: posts.count,
77
77
  buried_posts: posts.where(karma: ...0).count,
78
- daily_popular_rate: with[
78
+ daily_popular_rate: with_resolved[
79
79
  daily_posts.popular.count,
80
80
  daily_posts.count
81
81
  ] do |popular, total|
82
82
  total.map { |date, count| [date, (popular[date]||0).to_f / count] }.to_h
83
83
  end,
84
- dow_avg_comments: with[
84
+ dow_avg_comments: with_resolved[
85
85
  dow_not_buried.sum(:comments_count),
86
86
  dow_not_buried.count
87
87
  ] do |comments, posts|
@@ -94,4 +94,4 @@ end
94
94
 
95
95
  Since `summarize` runs a single query that visits each relevant `posts` row just once, adding additional calculations is pretty close to free.
96
96
 
97
- Even with the mental overhead of needing to join outside the block and use `with` to combine calculations (see [README](../README.md) for details), I think this is still easy to read, write, and reason about, and it beats the heck out of walls of SQL. What do you think?
97
+ Even with the mental overhead of needing to join outside the block and use `with_resolved` to combine calculations (see [README](../README.md) for details), I think this is still easy to read, write, and reason about, and it beats the heck out of walls of SQL. What do you think?
@@ -2,6 +2,6 @@
2
2
 
3
3
  module ActiveRecord
4
4
  module Summarize
5
- VERSION = "0.4.0"
5
+ VERSION = "0.5.1"
6
6
  end
7
7
  end
@@ -48,8 +48,8 @@ module ActiveRecord::Summarize
48
48
  end
49
49
 
50
50
  def process(&block)
51
- # For noop, just yield the original relation and a transparent `with` proc.
52
- return yield(@relation, ChainableResult::SYNC_WITH) if noop?
51
+ # For noop, just yield the original relation and a transparent `with_resolved` proc.
52
+ return yield(@relation, ChainableResult::SYNC_WITH_RESOLVED) if noop?
53
53
  # Within the block, the relation and its future clones intercept calls to
54
54
  # `count` and `sum`, registering them and returning a ChainableResult via
55
55
  # summarize.add_calculation.
@@ -60,7 +60,7 @@ module ActiveRecord::Summarize
60
60
  include InstanceMethods
61
61
  end
62
62
  end,
63
- ChainableResult::WITH
63
+ ChainableResult::WITH_RESOLVED
64
64
  ))
65
65
  ChainableResult.with_cache(!pure?) do
66
66
  # `resolve` builds the single query that answers all collected calculations,
@@ -240,7 +240,10 @@ module ActiveRecord::Summarize
240
240
  end
241
241
 
242
242
  def value_selects
243
- @calculations.map { |f| f.select_value(@relation) }
243
+ @calculations.each_with_index.map do |f, i|
244
+ f.select_value(@relation)
245
+ .as("_v#{i}") # In Postgres with certain Rails versions, alias is needed to disambiguate result column names for type information
246
+ end
244
247
  end
245
248
 
246
249
  def lightly_touch_impure_hash(h)
@@ -12,9 +12,19 @@ class ChainableResult
12
12
  if use_cache?
13
13
  return @value if @cached
14
14
  @cached = true
15
- @value = resolve_source.send(@method, *@args, **@opts, &@block)
15
+ @value = resolve_source.send(
16
+ @method,
17
+ *@args.map(&RESOLVE_ITEM),
18
+ **@opts.transform_values(&RESOLVE_ITEM),
19
+ &@block
20
+ )
16
21
  else
17
- resolve_source.send(@method, *@args, **@opts, &@block)
22
+ resolve_source.send(
23
+ @method,
24
+ *@args.map(&RESOLVE_ITEM),
25
+ **@opts.transform_values(&RESOLVE_ITEM),
26
+ &@block
27
+ )
18
28
  end
19
29
  end
20
30
 
@@ -78,16 +88,17 @@ class ChainableResult
78
88
  end
79
89
 
80
90
  def self.with(*results, &block)
81
- ChainableResult.wrap(results.size == 1 ? results.first : results, :then, &block)
91
+ ChainableResult.wrap((results.size == 1) ? results.first : results, :then, &block)
82
92
  end
83
93
 
84
94
  def self.sync_with(*results, &block)
85
95
  # Non-time-traveling, synchronous version of `with` for testing
86
- (results.size == 1 ? results.first : results).then(&block)
96
+ ((results.size == 1) ? results.first : results).then(&block)
87
97
  end
88
98
 
89
- WITH = method(:with)
90
- SYNC_WITH = method(:sync_with)
99
+ # Shorter names are deprecated
100
+ WITH_RESOLVED = WITH = method(:with)
101
+ SYNC_WITH_RESOLVED = SYNC_WITH = method(:sync_with)
91
102
 
92
103
  def self.resolve_item(item)
93
104
  case item
metadata CHANGED
@@ -1,14 +1,14 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: activerecord-summarize
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.4.0
4
+ version: 0.5.1
5
5
  platform: ruby
6
6
  authors:
7
7
  - Joshua Paine
8
8
  autorequire:
9
9
  bindir: exe
10
10
  cert_chain: []
11
- date: 2023-02-27 00:00:00.000000000 Z
11
+ date: 2023-08-16 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: activerecord
@@ -84,7 +84,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement
84
84
  - !ruby/object:Gem::Version
85
85
  version: '0'
86
86
  requirements: []
87
- rubygems_version: 3.3.3
87
+ rubygems_version: 3.3.7
88
88
  signing_key:
89
89
  specification_version: 4
90
90
  summary: Run many .count and/or .sum queries in a single efficient query with minimal