activerecord-summarize 0.3.1 → 0.4.0

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: d491ee7730156f77105ec7df6bac79b6410fa97b3387816f103dee233b43df8d
4
- data.tar.gz: f33be41270ab955fcf2bf9b227cc7c212ae3aa385786da3a5b424a3e1e707e47
3
+ metadata.gz: 0e87406ae4f6c3aeec411824af98bb21b12457815f24e09f446ed8576bfc1352
4
+ data.tar.gz: 266e9065f9e49e458fa427d3a82c55eae82eafd983c27a5c8729125c2c75eb57
5
5
  SHA512:
6
- metadata.gz: 0d1f5308da4fc8b781e8dd5a69a10c671106acdb9b565c056d23b33114fc96f8d4ff9a60f3887b861f9142b2564c3f33f17d9e8ea52c212c27abbdacc861e2d6
7
- data.tar.gz: 318bad930ac53001068e6e3396d921f71ff62c8a0899abe7a96a55d9dab59aa7e6e45c2cdc6e7a5f5ae60b67f18a47be79cb73ee29a85e1fac25a988c43dc47a
6
+ metadata.gz: b515f889c18886602e6a695982ad755bdbb44b38ec93a1422cc92fb0ae9da612e3a7f2854d12c07864bedde52134f121823da201a8f52383564643ca2a7b8e84
7
+ data.tar.gz: e67437ac8503938c9f6f671fcf7c952aed1998efb045ff6a05a7d2a16e2c50ed7d79519f93b8a0d196cbf7d9d1bd9c23673e51487f7be8de3554cb27dde264fc
data/CHANGELOG.md CHANGED
@@ -1,3 +1,15 @@
1
+ ## [0.4.0] - 2023-02-27
2
+
3
+ - **FEATURE:** Support for top-level .group(:belongs_to_association), returning hash with models as keys.
4
+
5
+ I didn't realize this until a few months ago, but in ActiveRecord, if `Foo belongs_to :bar`, you can do `Foo.group(:bar).count` and get back a hash with `Bar` records as keys and counts as values. (ActiveRecord executes two total queries to implement this, one for the counts grouped by the `bar_id` foreign key, then another to retrieve the `Bar` models.)
6
+
7
+ Now the same behavior works with `summarize`: you can still retrieve any number of counts and/or sums about `Foo`—including some with additional filters and even sub-grouping—in a single query, and then we'll execute one additional query to retrieve the records for the `Bar` model keys.
8
+
9
+ - **IMPROVEMENT:** `bin/console` is now much more useful for developing `activerecord-summarize`
10
+
11
+ - **IMPROVEMENT:** Added some tests for queries joining HABTM associations and (of course, supporting the new feature) `belongs_to` associations. `summarize` preceded by joins is already stable and documented, but it didn't have tests before.
12
+
1
13
  ## [0.3.1] - 2022-06-23
2
14
 
3
15
  - **BUGFIX:** `with` didn't work correctly with a single argument. Embarassingly, both the time-traveling version of `with` and the trivial/fake one provided when `noop: true` is set had single argument bugs, and they were different bugs.
data/Gemfile.lock CHANGED
@@ -1,7 +1,7 @@
1
1
  PATH
2
2
  remote: .
3
3
  specs:
4
- activerecord-summarize (0.3.1)
4
+ activerecord-summarize (0.4.0)
5
5
  activerecord (>= 5.0)
6
6
 
7
7
  GEM
data/bin/console CHANGED
@@ -2,10 +2,11 @@
2
2
  # frozen_string_literal: true
3
3
 
4
4
  require "bundler/setup"
5
+ require "active_record"
5
6
  require "activerecord/summarize"
7
+ require_relative "../test/test_data" # Test fixtures so there's something to play with
6
8
 
7
- # You can add fixtures and/or initialization code here to make experimenting
8
- # with your gem easier. You can also use a different console, if you like.
9
+ # You can use a different console, if you like.
9
10
 
10
11
  # (If you use this, don't forget to add pry to your Gemfile!)
11
12
  # require "pry"
@@ -2,6 +2,6 @@
2
2
 
3
3
  module ActiveRecord
4
4
  module Summarize
5
- VERSION = "0.3.1"
5
+ VERSION = "0.4.0"
6
6
  end
7
7
  end
@@ -7,7 +7,7 @@ module ActiveRecord::Summarize
7
7
  class Unsummarizable < StandardError; end
8
8
 
9
9
  class Summarize
10
- attr_reader :current_result_row, :pure, :noop, :from_where
10
+ attr_reader :current_result_row, :base_groups, :base_association, :pure, :noop, :from_where
11
11
  alias_method :pure?, :pure
12
12
  alias_method :noop?, :noop
13
13
 
@@ -29,7 +29,18 @@ module ActiveRecord::Summarize
29
29
  def initialize(relation, pure: nil, noop: false)
30
30
  @relation = relation
31
31
  @noop = noop
32
- has_base_groups = relation.group_values.any?
32
+ @base_groups, @base_association = relation.group_values.dup.then do |group_fields|
33
+ # Based upon a bit from ActiveRecord::Calculations.execute_grouped_calculation,
34
+ # if the base relation is grouped only by a belongs_to association, group by
35
+ # the association's foreign key.
36
+ if group_fields.size == 1 && group_fields.first.respond_to?(:to_sym)
37
+ association = relation.klass._reflect_on_association(group_fields.first)
38
+ # Like ActiveRecord's group(:association).count behavior, this only works with belongs_to associations
39
+ next [Array(association.foreign_key), association] if association&.belongs_to?
40
+ end
41
+ [group_fields, nil]
42
+ end
43
+ has_base_groups = base_groups.any?
33
44
  raise Unsummarizable, "`summarize` must be pure when called on a grouped relation" if pure == false && has_base_groups
34
45
  raise ArgumentError, "`summarize(noop: true)` is impossible on a grouped relation" if noop && has_base_groups
35
46
  @pure = has_base_groups || !!pure
@@ -53,24 +64,38 @@ module ActiveRecord::Summarize
53
64
  ))
54
65
  ChainableResult.with_cache(!pure?) do
55
66
  # `resolve` builds the single query that answers all collected calculations,
56
- # executes it, and aggregates the results by the values of
57
- # `@relation.group_values``. In the common case of no `@relation.group_values`,
58
- # the result is just `{[]=>[*final_value_for_each_calculation]}`
67
+ # executes it, and aggregates the results by the values of `base_groups`.
68
+ # In the common case of no `base_groups`, the resolve returns:
69
+ # `{[]=>[*final_value_for_each_calculation]}`
59
70
  result = resolve.transform_values! do |row|
60
71
  # Each row (in the common case, only one) is used to resolve any
61
72
  # ChainableResults returned by the block. These may be a one-to-one mapping,
62
- # or the block return may have combined some results via `with` or chained
73
+ # or the block return may have combined some results via `with`, chained
63
74
  # additional methods on results, etc..
64
75
  @current_result_row = row
65
76
  future_block_result.value
66
77
  end.then do |result|
67
- # Change ungrouped result from `{[]=>v}` to `v` and grouped-by-one-column
68
- # result from `{[k1]=>v1,[k2]=>v2,...}` to `{k1=>v1,k2=>v2,...}`.
69
- # (Those are both probably more common than multiple-column base grouping.)
70
- case @relation.group_values.size
71
- when 0 then result.values.first
72
- when 1 then result.transform_keys! { |k| k.first }
73
- else result
78
+ # Now unpack/fix-up the result keys to match shape of Relation.count or Relation.group(*cols).count return values
79
+ if base_groups.empty?
80
+ # Change ungrouped result from `{[]=>v}` to `v`, like Relation.count
81
+ result.values.first
82
+ elsif base_association
83
+ # Change grouped-by-one-belongs_to-association result from `{[id1]=>v1,[id2]=>v2,...}` to
84
+ # `{<AssociatedModel id:id1>=>v1,<AssociatedModel id:id2>=>v2,...}` like Relation.group(:association).count
85
+
86
+ # Loosely based on a bit from ActiveRecord::Calculations.execute_grouped_calculation,
87
+ # retrieve the records for the group association and replace the keys of our final result.
88
+ key_class = base_association.klass.base_class
89
+ key_records = key_class
90
+ .where(key_class.primary_key => result.keys.flatten)
91
+ .index_by(&:id)
92
+ result.transform_keys! { |k| key_records[k[0]] }
93
+ elsif base_groups.size == 1
94
+ # Change grouped-by-one-column result from `{[k1]=>v1,[k2]=>v2,...}` to `{k1=>v1,k2=>v2,...}`, like Relation.group(:column).count
95
+ result.transform_keys! { |k| k[0] }
96
+ else
97
+ # Multiple-column base grouping (though perhaps relatively rare) requires no change.
98
+ result
74
99
  end
75
100
  end
76
101
  if !pure?
@@ -166,7 +191,7 @@ module ActiveRecord::Summarize
166
191
  base_group_columns = (0...base_groups.size)
167
192
  data
168
193
  .group_by { |row| row[base_group_columns] }
169
- .tap { |h| h[[]] = [] if h.empty? && base_groups.size.zero? }
194
+ .tap { |h| h[[]] = [] if h.empty? && base_groups.empty? }
170
195
  .transform_values! do |rows|
171
196
  values = starting_values.map(&:dup) # map(&:dup) since some are hashes and we don't want to mutate starting_values
172
197
  rows.each do |row|
@@ -201,14 +226,10 @@ module ActiveRecord::Summarize
201
226
  end
202
227
  end
203
228
 
204
- def base_groups
205
- @relation.group_values.dup
206
- end
207
-
208
229
  def all_groups
209
230
  # keep all base groups, even if they did something silly like group by
210
231
  # the same key twice, but otherwise don't repeat any groups
211
- groups = base_groups
232
+ groups = base_groups.dup
212
233
  groups_set = Set.new(groups)
213
234
  @calculations.map { |f| f.relation.group_values }.flatten.each do |k|
214
235
  next if groups_set.include? k
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.3.1
4
+ version: 0.4.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Joshua Paine
8
8
  autorequire:
9
9
  bindir: exe
10
10
  cert_chain: []
11
- date: 2022-06-23 00:00:00.000000000 Z
11
+ date: 2023-02-27 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: activerecord