hightop 0.2.1 → 0.3.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: 7bfda8167d2141d8bd0be9678f1f285fe8417d0deecac5a04cee44f50efcf876
4
- data.tar.gz: 0414c92e5702679fc169dbecb03d754e79461968e97a39cd2cca15d94ec4daa3
3
+ metadata.gz: ee918ba56e73e37d1c623e99f4bf2971f1f99a82a369582eaf78144951dab192
4
+ data.tar.gz: c626b6953aae5d3e43e00291b5fba24d8326d133f5aba945ab58f59f4defcb65
5
5
  SHA512:
6
- metadata.gz: baad6fa1e6f4f404fd2f33448a971412ea9bf50257f5dd69531e341aa0a3afb69c816ac3191a9988ed8f30b19cce2c1d0837fb077e78fe589a7975b0283ec6d7
7
- data.tar.gz: a66db10388aff6057d63f9ae977a700c0065d222a52dd04df0e7ea4bbd87f5ee51a145ff875aea5b9042dbbbaff2a10543f612519b3c65ec525852728aef5f82
6
+ metadata.gz: e82a5854442ce691bc664cf6bb32aa1db21ec5e527fbc4c2ff9ddca49c8b278b8edc454cb0178ddc147ab47db324b091a65840783c9912a3051c16185bf45be3
7
+ data.tar.gz: '0978f96d0c782e7e6cb9221c11ea01f1ec55ebc1e287fec595416b587f64d30dbe8dcda0859a18d81445ea5900b21c3acf1838cb9c29592bfa76a192f7bd76d2'
data/CHANGELOG.md CHANGED
@@ -1,45 +1,66 @@
1
- ## 0.2.1
1
+ ## 0.3.0 (2021-08-12)
2
+
3
+ - Raise `ActiveRecord::UnknownAttributeReference` for non-attribute arguments
4
+ - Raise `ArgumentError` for too many arguments with enumerable
5
+ - Removed `uniq` option (use `distinct` instead)
6
+ - Dropped support for Active Record < 5.2 and Ruby < 2.6
7
+
8
+ ## 0.2.4 (2020-09-07)
9
+
10
+ - Added warning for non-attribute argument
11
+ - Added deprecation warning for `uniq`
12
+
13
+ ## 0.2.3 (2020-06-18)
14
+
15
+ - Dropped support for Active Record 4.2 and Ruby 2.3
16
+ - Fixed deprecation warning in Ruby 2.7
17
+
18
+ ## 0.2.2 (2019-08-12)
19
+
20
+ - Added support for Mongoid
21
+
22
+ ## 0.2.1 (2019-08-04)
2
23
 
3
24
  - Added support for arrays and hashes
4
25
 
5
- ## 0.2.0
26
+ ## 0.2.0 (2017-03-19)
6
27
 
7
28
  - Use keyword arguments
8
29
 
9
- ## 0.1.4
30
+ ## 0.1.4 (2016-02-04)
10
31
 
11
32
  - Added `distinct` option to replace `uniq`
12
33
 
13
- ## 0.1.3
34
+ ## 0.1.3 (2015-06-18)
14
35
 
15
36
  - Fixed `min` option with `uniq`
16
37
 
17
- ## 0.1.2
38
+ ## 0.1.2 (2014-11-05)
18
39
 
19
40
  - Added `min` option
20
41
 
21
- ## 0.1.1
42
+ ## 0.1.1 (2014-07-02)
22
43
 
23
44
  - Added `uniq` option
24
45
  - Fixed `Model.limit(n).top`
25
46
 
26
- ## 0.1.0
47
+ ## 0.1.0 (2014-06-11)
27
48
 
28
49
  - No changes, just bump
29
50
 
30
- ## 0.0.4
51
+ ## 0.0.4 (2014-06-11)
31
52
 
32
53
  - Added support for multiple groups
33
54
  - Added `nil` option
34
55
 
35
- ## 0.0.3
56
+ ## 0.0.3 (2014-06-11)
36
57
 
37
58
  - Fixed escaping
38
59
 
39
- ## 0.0.2
60
+ ## 0.0.2 (2014-05-29)
40
61
 
41
62
  - Added `limit` parameter
42
63
 
43
- ## 0.0.1
64
+ ## 0.0.1 (2014-05-11)
44
65
 
45
66
  - First release
data/LICENSE.txt CHANGED
@@ -1,4 +1,4 @@
1
- Copyright (c) 2014-2019 Andrew Kane
1
+ Copyright (c) 2014-2021 Andrew Kane
2
2
 
3
3
  MIT License
4
4
 
data/README.md CHANGED
@@ -4,23 +4,16 @@ A nice shortcut for group count queries
4
4
 
5
5
  ```ruby
6
6
  Visit.top(:browser)
7
+ # {
8
+ # "Chrome" => 63,
9
+ # "Safari" => 50,
10
+ # "Firefox" => 34
11
+ # }
7
12
  ```
8
13
 
9
- instead of
14
+ Works with Active Record, Mongoid, arrays and hashes
10
15
 
11
- ```ruby
12
- Visit.group(:browser).where("browser IS NOT NULL").order("count_all DESC, browser").count
13
- ```
14
-
15
- Be sure to [sanitize user input](https://rails-sqli.org/) like you must with `group`
16
-
17
- Also works with arrays and hashes
18
-
19
- ```ruby
20
- ["up", "up", "down"].top(1)
21
- ```
22
-
23
- [![Build Status](https://travis-ci.org/ankane/hightop.svg)](https://travis-ci.org/ankane/hightop)
16
+ [![Build Status](https://github.com/ankane/hightop/workflows/build/badge.svg?branch=master)](https://github.com/ankane/hightop/actions)
24
17
 
25
18
  ## Installation
26
19
 
@@ -100,6 +93,18 @@ Min count
100
93
  ["up", "up", "down"].top(min: 2)
101
94
  ```
102
95
 
96
+ ## Upgrading
97
+
98
+ ### 0.3.0
99
+
100
+ Hightop 0.3.0 protects against unsafe input by default. For non-attribute arguments, use:
101
+
102
+ ```ruby
103
+ Visit.top(Arel.sql(known_safe_value))
104
+ ```
105
+
106
+ Also, the `uniq` option has been removed. Use `distinct` instead.
107
+
103
108
  ## History
104
109
 
105
110
  View the [changelog](https://github.com/ankane/hightop/blob/master/CHANGELOG.md)
@@ -112,3 +117,12 @@ Everyone is encouraged to help improve this project. Here are a few ways you can
112
117
  - Fix bugs and [submit pull requests](https://github.com/ankane/hightop/pulls)
113
118
  - Write, clarify, or fix documentation
114
119
  - Suggest or add new features
120
+
121
+ To get started with development:
122
+
123
+ ```sh
124
+ git clone https://github.com/ankane/hightop.git
125
+ cd hightop
126
+ bundle install
127
+ bundle exec rake test
128
+ ```
data/lib/hightop.rb CHANGED
@@ -3,9 +3,15 @@ require "active_support"
3
3
 
4
4
  # modules
5
5
  require "hightop/enumerable"
6
- require "hightop/kicks"
7
6
  require "hightop/version"
8
7
 
9
8
  ActiveSupport.on_load(:active_record) do
9
+ require "hightop/utils"
10
+ require "hightop/kicks"
10
11
  extend Hightop::Kicks
11
12
  end
13
+
14
+ ActiveSupport.on_load(:mongoid) do
15
+ require "hightop/mongoid"
16
+ Mongoid::Document::ClassMethods.include(Hightop::Mongoid)
17
+ end
@@ -1,12 +1,9 @@
1
1
  module Enumerable
2
- def top(*args, &block)
3
- if block || !respond_to?(:scoping)
4
- limit, options, _ = args
5
- if limit.is_a?(Hash) && args.size == 1
6
- options = limit
7
- limit = nil
8
- end
9
- options ||= {}
2
+ def top(*args, **options, &block)
3
+ if block || !(respond_to?(:scoping) || respond_to?(:with_scope))
4
+ raise ArgumentError, "wrong number of arguments (given 2, expected 0..1)" if args.size > 1
5
+
6
+ limit = args[0]
10
7
  min = options[:min]
11
8
 
12
9
  counts = Hash.new(0)
@@ -19,8 +16,10 @@ module Enumerable
19
16
  arr = counts.sort_by { |_, v| -v }
20
17
  arr = arr[0...limit] if limit
21
18
  Hash[arr]
19
+ elsif respond_to?(:scoping)
20
+ scoping { @klass.send(:top, *args, **options, &block) }
22
21
  else
23
- scoping { @klass.send(:top, *args, &block) }
22
+ with_scope(self) { klass.send(:top, *args, **options, &block) }
24
23
  end
25
24
  end
26
25
  end
data/lib/hightop/kicks.rb CHANGED
@@ -1,22 +1,31 @@
1
1
  module Hightop
2
2
  module Kicks
3
- def top(column, limit = nil, distinct: nil, uniq: nil, min: nil, nil: nil)
4
- distinct ||= uniq
5
- order_str = column.is_a?(Array) ? column.map(&:to_s).join(", ") : column
6
- relation = group(column).order(["count_#{distinct || 'all'} DESC", order_str])
3
+ def top(column, limit = nil, distinct: nil, min: nil, nil: nil)
4
+ columns = column.is_a?(Array) ? column : [column]
5
+ columns = columns.map { |c| Utils.validate_column(c) }
6
+
7
+ distinct = Utils.validate_column(distinct) if distinct
8
+
9
+ relation = group(*columns).order("1 DESC", *columns)
7
10
  if limit
8
11
  relation = relation.limit(limit)
9
12
  end
10
13
 
11
14
  # terribly named option
12
15
  unless binding.local_variable_get(:nil)
13
- (column.is_a?(Array) ? column : [column]).each do |c|
16
+ columns.each do |c|
17
+ c = Utils.resolve_column(self, c)
14
18
  relation = relation.where("#{c} IS NOT NULL")
15
19
  end
16
20
  end
17
21
 
18
22
  if min
19
- relation = relation.having("COUNT(#{distinct ? "DISTINCT #{distinct}" : '*'}) >= #{min.to_i}")
23
+ if distinct
24
+ d = Utils.resolve_column(self, distinct)
25
+ relation = relation.having("COUNT(DISTINCT #{d}) >= #{min.to_i}")
26
+ else
27
+ relation = relation.having("COUNT(*) >= #{min.to_i}")
28
+ end
20
29
  end
21
30
 
22
31
  if distinct
@@ -0,0 +1,52 @@
1
+ module Hightop
2
+ module Mongoid
3
+ # super helpful article
4
+ # https://maximomussini.com/posts/mongoid-aggregation-dsl/
5
+ def top(column, limit = nil, distinct: nil, min: nil, nil: nil)
6
+ columns = column.is_a?(Array) ? column : [column]
7
+
8
+ relation = all
9
+
10
+ # terribly named option
11
+ unless binding.local_variable_get(:nil)
12
+ columns.each do |c|
13
+ relation = relation.and(c.ne => nil)
14
+ end
15
+ end
16
+
17
+ ids = {}
18
+ columns.each_with_index do |c, i|
19
+ ids["c#{i}"] = "$#{c}"
20
+ end
21
+
22
+ if distinct
23
+ # group with distinct column first, then group without it
24
+ # https://stackoverflow.com/questions/24761266/select-group-by-count-and-distinct-count-in-same-mongodb-query/24770233#24770233
25
+ distinct_ids = ids.merge("c#{ids.size}" => "$#{distinct}")
26
+ relation = relation.group(_id: distinct_ids, count: {"$sum" => 1})
27
+ ids.each_key do |k|
28
+ ids[k] = "$_id.#{k}"
29
+ end
30
+ end
31
+
32
+ relation = relation.group(_id: ids, count: {"$sum" => 1})
33
+
34
+ if min
35
+ relation.pipeline.push("$match" => {"count" => {"$gte" => min}})
36
+ end
37
+
38
+ relation = relation.desc(:count)
39
+ if limit
40
+ relation = relation.limit(limit)
41
+ end
42
+
43
+ result = {}
44
+ collection.aggregate(relation.pipeline).each do |doc|
45
+ key = doc["_id"].values
46
+ key = key[0] if key.size == 1
47
+ result[key] = doc["count"]
48
+ end
49
+ result
50
+ end
51
+ end
52
+ end
@@ -0,0 +1,25 @@
1
+ module Hightop
2
+ module Utils
3
+ class << self
4
+ # basic version of Active Record disallow_raw_sql!
5
+ # symbol = column (safe), Arel node = SQL (safe), other = untrusted
6
+ # matches table.column and column
7
+ def validate_column(column)
8
+ unless column.is_a?(Symbol) || column.is_a?(Arel::Nodes::SqlLiteral)
9
+ column = column.to_s
10
+ unless /\A\w+(\.\w+)?\z/i.match(column)
11
+ raise ActiveRecord::UnknownAttributeReference, "Query method called with non-attribute argument(s): #{column.inspect}. Use Arel.sql() for known-safe values."
12
+ end
13
+ end
14
+ column
15
+ end
16
+
17
+ # resolves eagerly
18
+ def resolve_column(relation, column)
19
+ node = relation.send(:relation).send(:arel_columns, [column]).first
20
+ node = Arel::Nodes::SqlLiteral.new(node) if node.is_a?(String)
21
+ relation.connection.visitor.accept(node, Arel::Collectors::SQLString.new).value
22
+ end
23
+ end
24
+ end
25
+ end
@@ -1,3 +1,3 @@
1
1
  module Hightop
2
- VERSION = "0.2.1"
2
+ VERSION = "0.3.0"
3
3
  end
metadata CHANGED
@@ -1,14 +1,14 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: hightop
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.2.1
4
+ version: 0.3.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Andrew Kane
8
- autorequire:
8
+ autorequire:
9
9
  bindir: bin
10
10
  cert_chain: []
11
- date: 2019-08-05 00:00:00.000000000 Z
11
+ date: 2021-08-13 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: activesupport
@@ -16,72 +16,16 @@ dependencies:
16
16
  requirements:
17
17
  - - ">="
18
18
  - !ruby/object:Gem::Version
19
- version: '4.2'
19
+ version: '5.2'
20
20
  type: :runtime
21
21
  prerelease: false
22
22
  version_requirements: !ruby/object:Gem::Requirement
23
23
  requirements:
24
24
  - - ">="
25
25
  - !ruby/object:Gem::Version
26
- version: '4.2'
27
- - !ruby/object:Gem::Dependency
28
- name: bundler
29
- requirement: !ruby/object:Gem::Requirement
30
- requirements:
31
- - - ">="
32
- - !ruby/object:Gem::Version
33
- version: '0'
34
- type: :development
35
- prerelease: false
36
- version_requirements: !ruby/object:Gem::Requirement
37
- requirements:
38
- - - ">="
39
- - !ruby/object:Gem::Version
40
- version: '0'
41
- - !ruby/object:Gem::Dependency
42
- name: rake
43
- requirement: !ruby/object:Gem::Requirement
44
- requirements:
45
- - - ">="
46
- - !ruby/object:Gem::Version
47
- version: '0'
48
- type: :development
49
- prerelease: false
50
- version_requirements: !ruby/object:Gem::Requirement
51
- requirements:
52
- - - ">="
53
- - !ruby/object:Gem::Version
54
- version: '0'
55
- - !ruby/object:Gem::Dependency
56
- name: minitest
57
- requirement: !ruby/object:Gem::Requirement
58
- requirements:
59
- - - ">="
60
- - !ruby/object:Gem::Version
61
- version: '5'
62
- type: :development
63
- prerelease: false
64
- version_requirements: !ruby/object:Gem::Requirement
65
- requirements:
66
- - - ">="
67
- - !ruby/object:Gem::Version
68
- version: '5'
69
- - !ruby/object:Gem::Dependency
70
- name: sqlite3
71
- requirement: !ruby/object:Gem::Requirement
72
- requirements:
73
- - - ">="
74
- - !ruby/object:Gem::Version
75
- version: '0'
76
- type: :development
77
- prerelease: false
78
- version_requirements: !ruby/object:Gem::Requirement
79
- requirements:
80
- - - ">="
81
- - !ruby/object:Gem::Version
82
- version: '0'
83
- description:
84
- email: andrew@chartkick.com
26
+ version: '5.2'
27
+ description:
28
+ email: andrew@ankane.org
85
29
  executables: []
86
30
  extensions: []
87
31
  extra_rdoc_files: []
@@ -92,12 +36,14 @@ files:
92
36
  - lib/hightop.rb
93
37
  - lib/hightop/enumerable.rb
94
38
  - lib/hightop/kicks.rb
39
+ - lib/hightop/mongoid.rb
40
+ - lib/hightop/utils.rb
95
41
  - lib/hightop/version.rb
96
42
  homepage: https://github.com/ankane/hightop
97
43
  licenses:
98
44
  - MIT
99
45
  metadata: {}
100
- post_install_message:
46
+ post_install_message:
101
47
  rdoc_options: []
102
48
  require_paths:
103
49
  - lib
@@ -105,15 +51,15 @@ required_ruby_version: !ruby/object:Gem::Requirement
105
51
  requirements:
106
52
  - - ">="
107
53
  - !ruby/object:Gem::Version
108
- version: '2.3'
54
+ version: '2.6'
109
55
  required_rubygems_version: !ruby/object:Gem::Requirement
110
56
  requirements:
111
57
  - - ">="
112
58
  - !ruby/object:Gem::Version
113
59
  version: '0'
114
60
  requirements: []
115
- rubygems_version: 3.0.4
116
- signing_key:
61
+ rubygems_version: 3.2.22
62
+ signing_key:
117
63
  specification_version: 4
118
64
  summary: A nice shortcut for group count queries
119
65
  test_files: []