activerecord-summarize 0.2.2 → 0.3.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: 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