activerecord-summarize 0.3.1 → 0.4.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 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