hightop 0.2.1 → 0.3.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: 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: []