activerecord-summarize 0.2.2 → 0.3.1

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: 8bd10ad02402bdba82cf06beb2d45b91002a2f94eb74322d86f4457360fa46ba
4
- data.tar.gz: 7d1f8d4cec9fd857d1551a527af7dbab701900ee2530490581b4d29ee6ef1e2f
3
+ metadata.gz: d491ee7730156f77105ec7df6bac79b6410fa97b3387816f103dee233b43df8d
4
+ data.tar.gz: f33be41270ab955fcf2bf9b227cc7c212ae3aa385786da3a5b424a3e1e707e47
5
5
  SHA512:
6
- metadata.gz: 3a39f8e9b7ff2ffb0bd142ca9c11ac810ef7f2d55c5a1a79d147f8c78eafc14e83a3f4215b6c0d37ae8cffe7e789de6e39ce8680dbf3449c6244b7292554a46a
7
- data.tar.gz: b74a44bd31888fd681bb4e5d0fd6b605b935b794dc39334e18f3a533850097b74410d567b522f05ce38a841336891a35e87f68c849a524cbd8e4e42785e9746e
6
+ metadata.gz: 0d1f5308da4fc8b781e8dd5a69a10c671106acdb9b565c056d23b33114fc96f8d4ff9a60f3887b861f9142b2564c3f33f17d9e8ea52c212c27abbdacc861e2d6
7
+ data.tar.gz: 318bad930ac53001068e6e3396d921f71ff62c8a0899abe7a96a55d9dab59aa7e6e45c2cdc6e7a5f5ae60b67f18a47be79cb73ee29a85e1fac25a988c43dc47a
data/CHANGELOG.md CHANGED
@@ -1,3 +1,24 @@
1
+ ## [0.3.1] - 2022-06-23
2
+
3
+ - **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.
4
+ - **IMPROVEMENT:** Automated tests covering every `with` invocation style I can think of for both implementations and a number of new tests to confirm that `noop: true` produces the same results as (default) `noop: false`.
5
+ - **IMPROVEMENT:** After the initial release I forgot I had a CHANGELOG, and now I've back-filled it.
6
+
7
+ ## [0.3.0] - 2022-06-04
8
+
9
+ - **BUGFIX:** `.sum(:foo)` of no rows or of all-null values now returns 0 instead of failing (completing partial fix from 0.2.3)
10
+ - **BREAKING:** extremely unlikely to actually break anything, but `.count("distinct id")` now raises as `.distinct.count(:id)` already did. (AFAICT, by the nature of the techniques underlying `summarize`, `distinct` counts cannot be supported.)
11
+ - **IMPROVEMENT:** Automated tests covering basic functions and past problem areas.
12
+
13
+ ## [0.2.3] - 2022-05-01
14
+
15
+ - **BUGFIX:** Fix results for SQL `SUM(null)` (n.b., this turned out to be only a partial fix)
16
+ - **BUGFIX:** Support summarize with only one query (not often very useful, but it should work!)
17
+
18
+ ## [0.2.2] - 2022-04-29
19
+
20
+ - **BUGFIX:** Incorrect Arel generation when using `.where(*anything).sum` inside a `summarize` block
21
+
1
22
  ## [0.2.1] - 2022-02-17
2
23
 
3
24
  - Initial public release
data/Gemfile CHANGED
@@ -6,7 +6,8 @@ source "https://rubygems.org"
6
6
  gemspec
7
7
 
8
8
  gem "rake", "~> 13.0"
9
-
10
9
  gem "minitest", "~> 5.0"
11
-
12
10
  gem "standard", "~> 1.3"
11
+
12
+ gem "activerecord", "7.0.3"
13
+ gem "sqlite3", "1.4.2"
data/Gemfile.lock CHANGED
@@ -1,24 +1,24 @@
1
1
  PATH
2
2
  remote: .
3
3
  specs:
4
- activerecord-summarize (0.2.2)
4
+ activerecord-summarize (0.3.1)
5
5
  activerecord (>= 5.0)
6
6
 
7
7
  GEM
8
8
  remote: https://rubygems.org/
9
9
  specs:
10
- activemodel (7.0.2.2)
11
- activesupport (= 7.0.2.2)
12
- activerecord (7.0.2.2)
13
- activemodel (= 7.0.2.2)
14
- activesupport (= 7.0.2.2)
15
- activesupport (7.0.2.2)
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)
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.9)
21
+ concurrent-ruby (1.1.10)
22
22
  i18n (1.10.0)
23
23
  concurrent-ruby (~> 1.0)
24
24
  minitest (5.15.0)
@@ -44,6 +44,7 @@ GEM
44
44
  rubocop (>= 1.7.0, < 2.0)
45
45
  rubocop-ast (>= 0.4.0)
46
46
  ruby-progressbar (1.11.0)
47
+ sqlite3 (1.4.2)
47
48
  standard (1.7.2)
48
49
  rubocop (= 1.25.1)
49
50
  rubocop-performance (= 1.13.2)
@@ -56,9 +57,11 @@ PLATFORMS
56
57
  x86_64-linux
57
58
 
58
59
  DEPENDENCIES
60
+ activerecord (= 7.0.3)
59
61
  activerecord-summarize!
60
62
  minitest (~> 5.0)
61
63
  rake (~> 13.0)
64
+ sqlite3 (= 1.4.2)
62
65
  standard (~> 1.3)
63
66
 
64
67
  BUNDLED WITH
@@ -24,7 +24,7 @@ Gem::Specification.new do |spec|
24
24
  # The `git ls-files -z` loads the files in the RubyGem that have been added into git.
25
25
  spec.files = Dir.chdir(File.expand_path(__dir__)) do
26
26
  `git ls-files -z`.split("\x0").reject do |f|
27
- (f == __FILE__) || f.match(%r{\A(?:(?:test|spec|features)/|\.(?:git|travis|circleci)|appveyor)})
27
+ (f == __FILE__) || f.match(%r{\A(?:(?:test|spec|features)/|\.(?:git|travis|circleci|vscode|standard)|appveyor)})
28
28
  end
29
29
  end
30
30
  spec.bindir = "exe"
@@ -11,6 +11,8 @@ For each subreddit that a user moderates, the user should see these stats with r
11
11
  - count of how many posts from this period were buried, i.e., ended up with negative karma
12
12
  - grouped by post creation date, the percentage of posts that ended up being popular, where popular means having a karma score greater than a per-subreddit-configured threshold
13
13
  - grouped by post creation day of the week, the average number of comments per non-buried post
14
+
15
+ > *Below, grouping by day of the week is handled with `.group("EXTRACT(DOW FROM posts.created_at)")`*
14
16
 
15
17
  ## Background
16
18
 
@@ -2,6 +2,6 @@
2
2
 
3
3
  module ActiveRecord
4
4
  module Summarize
5
- VERSION = "0.2.2"
5
+ VERSION = "0.3.1"
6
6
  end
7
7
  end
@@ -38,7 +38,7 @@ module ActiveRecord::Summarize
38
38
 
39
39
  def process(&block)
40
40
  # For noop, just yield the original relation and a transparent `with` proc.
41
- return yield(@relation, ->(*results, &block) { [*results].then(&block) }) if noop?
41
+ return yield(@relation, ChainableResult::SYNC_WITH) if noop?
42
42
  # Within the block, the relation and its future clones intercept calls to
43
43
  # `count` and `sum`, registering them and returning a ChainableResult via
44
44
  # summarize.add_calculation.
@@ -127,28 +127,36 @@ module ActiveRecord::Summarize
127
127
  # grouped_query = groups.any? ? from_where.group(*groups) : from_where
128
128
  grouped_query = groups.any? ? from_where.group(*1..groups.size) : from_where
129
129
  data = grouped_query.pluck(*groups, *value_selects)
130
+ # .pluck(:one_column) returns an array of values instead of an array of arrays,
131
+ # which breaks the aggregation and assignment below in case anyone ever asks
132
+ # `summarize` for only one thing.
133
+ data = data.map { |d| [d] } if (groups.size + value_selects.size) == 1
130
134
 
131
135
  # Aggregate & assign results
132
136
  group_idx = groups.each_with_index.to_h
133
137
  starting_values, reducers = @calculations.each_with_index.map do |f, i|
134
138
  value_column = groups.size + i
135
139
  group_columns = f.relation.group_values.map { |k| group_idx[k] }
140
+ # `row[value_column] || 0` pattern in reducers because SQL SUM(NULL)
141
+ # returns NULL, but like ActiveRecord we always want .sum to return a
142
+ # number, and our "starting_values and reducers" implementation means
143
+ # we sometimes will have to add NULL to our numbers.
136
144
  case group_columns.size
137
145
  when 0 then [
138
146
  0,
139
- ->(memo, row) { memo + row[value_column] }
147
+ ->(memo, row) { memo + (row[value_column] || 0) }
140
148
  ]
141
149
  when 1 then [
142
150
  Hash.new(0), # Default 0 makes the reducer much cleaner, but we have to clean it up later
143
151
  ->(memo, row) {
144
- memo[row[group_columns[0]]] += row[value_column] unless row[value_column].zero?
152
+ memo[row[group_columns[0]]] += row[value_column] unless (row[value_column] || 0).zero?
145
153
  memo
146
154
  }
147
155
  ]
148
156
  else [
149
157
  Hash.new(0),
150
158
  ->(memo, row) {
151
- memo[group_columns.map { |i| row[i] }] += row[value_column] unless row[value_column].zero?
159
+ memo[group_columns.map { |i| row[i] }] += row[value_column] unless (row[value_column] || 0).zero?
152
160
  memo
153
161
  }
154
162
  ]
@@ -233,14 +241,14 @@ module ActiveRecord::Summarize
233
241
  def select_value(base_relation)
234
242
  where = relation.where_clause - base_relation.where_clause
235
243
  for_select = column
236
- for_select = Arel::Nodes::Case.new(where.ast).when(true, for_select).else(unmatch_value) unless where.empty?
244
+ for_select = Arel::Nodes::Case.new(where.ast).when(true, for_select).else(unmatch_arel_node) unless where.empty?
237
245
  function.new([for_select]).tap { |f| f.distinct = relation.distinct_value }
238
246
  end
239
247
 
240
- def unmatch_value
248
+ def unmatch_arel_node
241
249
  case method
242
- when "sum" then 0
243
- when "count" then nil
250
+ when "sum" then 0 # Adding zero to a sum does nothing
251
+ when "count" then nil # In SQL, null is no value and is not counted
244
252
  else raise "Unknown calculation method"
245
253
  end
246
254
  end
@@ -268,6 +276,7 @@ module ActiveRecord::Summarize
268
276
  case operation = operation.to_s.downcase
269
277
  when "count", "sum"
270
278
  column_name = :id if [nil, "*", :all].include? column_name
279
+ raise Unsummarizable, "DISTINCT in SQL is not reliably correct with summarize" if column_name.is_a?(String) && /\bdistinct\b/i === column_name
271
280
  @summarize.add_calculation(self, operation, aggregate_column(column_name))
272
281
  else super
273
282
  end
@@ -69,7 +69,7 @@ class ChainableResult
69
69
  def self.wrap(v, method = nil, *args, **opts, &block)
70
70
  method ||= block ? :then : :itself
71
71
  klass = case v
72
- when ChainableResult then return v # don't wrap, exit early
72
+ when ChainableResult then ChainableResult::Future
73
73
  when ::Array then ChainableResult::Array
74
74
  when ::Hash then ChainableResult::Hash
75
75
  else ChainableResult::Other
@@ -81,7 +81,13 @@ class ChainableResult
81
81
  ChainableResult.wrap(results.size == 1 ? results.first : results, :then, &block)
82
82
  end
83
83
 
84
+ def self.sync_with(*results, &block)
85
+ # Non-time-traveling, synchronous version of `with` for testing
86
+ (results.size == 1 ? results.first : results).then(&block)
87
+ end
88
+
84
89
  WITH = method(:with)
90
+ SYNC_WITH = method(:sync_with)
85
91
 
86
92
  def self.resolve_item(item)
87
93
  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.2.2
4
+ version: 0.3.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: 2022-04-29 00:00:00.000000000 Z
11
+ date: 2022-06-23 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: activerecord
@@ -46,7 +46,6 @@ executables: []
46
46
  extensions: []
47
47
  extra_rdoc_files: []
48
48
  files:
49
- - ".standard.yml"
50
49
  - CHANGELOG.md
51
50
  - Gemfile
52
51
  - Gemfile.lock
data/.standard.yml DELETED
@@ -1,5 +0,0 @@
1
- # For available configuration options, see:
2
- # https://github.com/testdouble/standard
3
- # ignore:
4
- # - 'lib/**/*':
5
- # - Layout/DotPosition