├── lib ├── hightop │ ├── version.rb │ ├── enumerable.rb │ ├── utils.rb │ ├── kicks.rb │ └── mongoid.rb └── hightop.rb ├── gemfiles ├── mongoid8.gemfile └── mongoid9.gemfile ├── Rakefile ├── .gitignore ├── test ├── test_helper.rb ├── support │ ├── mongoid.rb │ └── active_record.rb ├── enumerable_test.rb └── model_test.rb ├── Gemfile ├── hightop.gemspec ├── .github └── workflows │ └── build.yml ├── LICENSE.txt ├── CHANGELOG.md └── README.md /lib/hightop/version.rb: -------------------------------------------------------------------------------- 1 | module Hightop 2 | VERSION = "0.6.0" 3 | end 4 | -------------------------------------------------------------------------------- /gemfiles/mongoid8.gemfile: -------------------------------------------------------------------------------- 1 | source "https://rubygems.org" 2 | 3 | gemspec path: ".." 4 | 5 | gem "rake" 6 | gem "minitest" 7 | gem "mongoid", "~> 8" 8 | -------------------------------------------------------------------------------- /gemfiles/mongoid9.gemfile: -------------------------------------------------------------------------------- 1 | source "https://rubygems.org" 2 | 3 | gemspec path: ".." 4 | 5 | gem "rake" 6 | gem "minitest" 7 | gem "mongoid", "~> 9" 8 | -------------------------------------------------------------------------------- /Rakefile: -------------------------------------------------------------------------------- 1 | require "bundler/gem_tasks" 2 | require "rake/testtask" 3 | 4 | Rake::TestTask.new do |t| 5 | t.pattern = "test/**/*_test.rb" 6 | end 7 | 8 | task default: :test 9 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | *.gem 2 | *.rbc 3 | .bundle 4 | .config 5 | .yardoc 6 | Gemfile.lock 7 | InstalledFiles 8 | _yardoc 9 | coverage 10 | doc/ 11 | lib/bundler/man 12 | pkg 13 | rdoc 14 | spec/reports 15 | test/tmp 16 | test/version_tmp 17 | tmp 18 | *.bundle 19 | *.so 20 | *.o 21 | *.a 22 | mkmf.log 23 | *.lock 24 | -------------------------------------------------------------------------------- /test/test_helper.rb: -------------------------------------------------------------------------------- 1 | require "bundler/setup" 2 | Bundler.require(:default) 3 | require "minitest/autorun" 4 | 5 | $logger = ActiveSupport::Logger.new(ENV["VERBOSE"] ? STDOUT : nil) 6 | 7 | if defined?(Mongoid) 8 | require_relative "support/mongoid" 9 | else 10 | require_relative "support/active_record" 11 | end 12 | -------------------------------------------------------------------------------- /test/support/mongoid.rb: -------------------------------------------------------------------------------- 1 | Mongoid.logger = $logger 2 | Mongo::Logger.logger = $logger if defined?(Mongo::Logger) 3 | 4 | Mongoid.configure do |config| 5 | config.connect_to "hightop_test" 6 | end 7 | 8 | class Visit 9 | include Mongoid::Document 10 | 11 | field :city, type: String 12 | field :user_id, type: String 13 | end 14 | -------------------------------------------------------------------------------- /Gemfile: -------------------------------------------------------------------------------- 1 | source "https://rubygems.org" 2 | 3 | gemspec 4 | 5 | gem "rake" 6 | gem "minitest" 7 | 8 | ar_version = ENV["AR_VERSION"] || "8.1.0" 9 | gem "activerecord", "~> #{ar_version}" 10 | 11 | case ENV["ADAPTER"] 12 | when "postgresql" 13 | gem "pg" 14 | when "mysql" 15 | gem "mysql2" 16 | when "trilogy" 17 | gem "trilogy" 18 | else 19 | gem "sqlite3", platform: :ruby 20 | gem "sqlite3-ffi", platform: :jruby 21 | end 22 | -------------------------------------------------------------------------------- /lib/hightop.rb: -------------------------------------------------------------------------------- 1 | # dependencies 2 | require "active_support" 3 | 4 | # modules 5 | require_relative "hightop/enumerable" 6 | require_relative "hightop/version" 7 | 8 | ActiveSupport.on_load(:active_record) do 9 | require_relative "hightop/utils" 10 | require_relative "hightop/kicks" 11 | extend Hightop::Kicks 12 | end 13 | 14 | ActiveSupport.on_load(:mongoid) do 15 | require_relative "hightop/mongoid" 16 | Mongoid::Document::ClassMethods.include(Hightop::Mongoid) 17 | end 18 | -------------------------------------------------------------------------------- /hightop.gemspec: -------------------------------------------------------------------------------- 1 | require_relative "lib/hightop/version" 2 | 3 | Gem::Specification.new do |spec| 4 | spec.name = "hightop" 5 | spec.version = Hightop::VERSION 6 | spec.summary = "A nice shortcut for group count queries" 7 | spec.homepage = "https://github.com/ankane/hightop" 8 | spec.license = "MIT" 9 | 10 | spec.author = "Andrew Kane" 11 | spec.email = "andrew@ankane.org" 12 | 13 | spec.files = Dir["*.{md,txt}", "{lib}/**/*"] 14 | spec.require_path = "lib" 15 | 16 | spec.required_ruby_version = ">= 3.2" 17 | 18 | spec.add_dependency "activesupport", ">= 7.1" 19 | end 20 | -------------------------------------------------------------------------------- /lib/hightop/enumerable.rb: -------------------------------------------------------------------------------- 1 | module Enumerable 2 | def top(*args, **options, &block) 3 | if block || !(respond_to?(:scoping) || respond_to?(:with_scope)) 4 | raise ArgumentError, "wrong number of arguments (given #{args.size}, expected 0..1)" if args.size > 1 5 | 6 | limit = args[0] 7 | min = options[:min] 8 | 9 | counts = Hash.new(0) 10 | map(&block).each do |v| 11 | counts[v] += 1 12 | end 13 | counts.delete(nil) unless options[:nil] 14 | counts.select! { |_, v| v >= min } if min 15 | 16 | arr = counts.sort_by { |_, v| -v } 17 | arr = arr[0...limit] if limit 18 | Hash[arr] 19 | elsif respond_to?(:scoping) 20 | scoping { klass.send(:top, *args, **options, &block) } 21 | else 22 | with_scope(self) { klass.send(:top, *args, **options, &block) } 23 | end 24 | end 25 | end 26 | -------------------------------------------------------------------------------- /test/support/active_record.rb: -------------------------------------------------------------------------------- 1 | require "active_record" 2 | 3 | # for debugging 4 | ActiveRecord::Base.logger = $logger 5 | ActiveRecord::Migration.verbose = ENV["VERBOSE"] 6 | 7 | # migrations 8 | case ENV["ADAPTER"] 9 | when "postgresql" 10 | ActiveRecord::Base.establish_connection adapter: "postgresql", database: "hightop_test" 11 | when "mysql" 12 | ActiveRecord::Base.establish_connection adapter: "mysql2", database: "hightop_test" 13 | when "trilogy" 14 | ActiveRecord::Base.establish_connection adapter: "trilogy", database: "hightop_test", host: "127.0.0.1" 15 | else 16 | ActiveRecord::Base.establish_connection adapter: "sqlite3", database: ":memory:" 17 | end 18 | 19 | ActiveRecord::Schema.define do 20 | create_table :visits, force: true do |t| 21 | t.string :city 22 | t.string :user_id 23 | end 24 | end 25 | 26 | class Visit < ActiveRecord::Base 27 | end 28 | -------------------------------------------------------------------------------- /.github/workflows/build.yml: -------------------------------------------------------------------------------- 1 | name: build 2 | on: [push, pull_request] 3 | jobs: 4 | build: 5 | strategy: 6 | fail-fast: false 7 | matrix: 8 | include: 9 | - ruby: 3.4 10 | ar_version: 8.1.0 11 | - ruby: 3.4 12 | ar_version: 8.0.0 13 | - ruby: 3.3 14 | ar_version: 7.2.0 15 | - ruby: 3.2 16 | ar_version: 7.1.0 17 | - ruby: 3.3 18 | gemfile: gemfiles/mongoid9.gemfile 19 | mongodb: true 20 | - ruby: 3.2 21 | gemfile: gemfiles/mongoid8.gemfile 22 | mongodb: true 23 | runs-on: ubuntu-latest 24 | env: 25 | AR_VERSION: ${{ matrix.ar_version || 'none' }} 26 | BUNDLE_GEMFILE: ${{ matrix.gemfile || 'Gemfile' }} 27 | steps: 28 | - uses: actions/checkout@v5 29 | - uses: ruby/setup-ruby@v1 30 | with: 31 | ruby-version: ${{ matrix.ruby }} 32 | bundler-cache: true 33 | - if: ${{ matrix.mongodb }} 34 | uses: ankane/setup-mongodb@v1 35 | - run: bundle exec rake test 36 | -------------------------------------------------------------------------------- /lib/hightop/utils.rb: -------------------------------------------------------------------------------- 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_pool.with_connection { |c| c.visitor.accept(node, Arel::Collectors::SQLString.new).value } 22 | end 23 | end 24 | end 25 | end 26 | -------------------------------------------------------------------------------- /LICENSE.txt: -------------------------------------------------------------------------------- 1 | Copyright (c) 2014-2024 Andrew Kane 2 | 3 | MIT License 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining 6 | a copy of this software and associated documentation files (the 7 | "Software"), to deal in the Software without restriction, including 8 | without limitation the rights to use, copy, modify, merge, publish, 9 | distribute, sublicense, and/or sell copies of the Software, and to 10 | permit persons to whom the Software is furnished to do so, subject to 11 | the following conditions: 12 | 13 | The above copyright notice and this permission notice shall be 14 | included in all copies or substantial portions of the Software. 15 | 16 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, 17 | EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF 18 | MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND 19 | NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE 20 | LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION 21 | OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION 22 | WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 23 | -------------------------------------------------------------------------------- /lib/hightop/kicks.rb: -------------------------------------------------------------------------------- 1 | module Hightop 2 | module Kicks 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) 10 | if limit 11 | relation = relation.limit(limit) 12 | end 13 | 14 | # terribly named option 15 | unless binding.local_variable_get(:nil) 16 | columns.each do |c| 17 | c = Utils.resolve_column(self, c) 18 | relation = relation.where("#{c} IS NOT NULL") 19 | end 20 | end 21 | 22 | if min 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 29 | end 30 | 31 | if distinct 32 | relation.distinct.count(distinct) 33 | else 34 | relation.count 35 | end 36 | end 37 | end 38 | end 39 | -------------------------------------------------------------------------------- /lib/hightop/mongoid.rb: -------------------------------------------------------------------------------- 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 53 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | ## 0.6.0 (2025-05-04) 2 | 3 | - Dropped support for Active Record < 7.1 and Ruby < 3.2 4 | - Dropped support for Mongoid < 8 5 | 6 | ## 0.5.0 (2024-10-07) 7 | 8 | - Fixed connection leasing for Active Record 7.2+ 9 | - Dropped support for Active Record < 7 and Ruby < 3.1 10 | 11 | ## 0.4.0 (2023-07-02) 12 | 13 | - Dropped support for Active Record < 6.1 and Ruby < 3 14 | - Dropped support for Mongoid < 7 15 | 16 | ## 0.3.0 (2021-08-12) 17 | 18 | - Raise `ActiveRecord::UnknownAttributeReference` for non-attribute arguments 19 | - Raise `ArgumentError` for too many arguments with arrays and hashes 20 | - Removed `uniq` option (use `distinct` instead) 21 | - Dropped support for Active Record < 5.2 and Ruby < 2.6 22 | 23 | ## 0.2.4 (2020-09-07) 24 | 25 | - Added warning for non-attribute argument 26 | - Added deprecation warning for `uniq` 27 | 28 | ## 0.2.3 (2020-06-18) 29 | 30 | - Dropped support for Active Record 4.2 and Ruby 2.3 31 | - Fixed deprecation warning in Ruby 2.7 32 | 33 | ## 0.2.2 (2019-08-12) 34 | 35 | - Added support for Mongoid 36 | 37 | ## 0.2.1 (2019-08-04) 38 | 39 | - Added support for arrays and hashes 40 | 41 | ## 0.2.0 (2017-03-19) 42 | 43 | - Use keyword arguments 44 | 45 | ## 0.1.4 (2016-02-04) 46 | 47 | - Added `distinct` option to replace `uniq` 48 | 49 | ## 0.1.3 (2015-06-18) 50 | 51 | - Fixed `min` option with `uniq` 52 | 53 | ## 0.1.2 (2014-11-05) 54 | 55 | - Added `min` option 56 | 57 | ## 0.1.1 (2014-07-02) 58 | 59 | - Added `uniq` option 60 | - Fixed `Model.limit(n).top` 61 | 62 | ## 0.1.0 (2014-06-11) 63 | 64 | - No changes, just bump 65 | 66 | ## 0.0.4 (2014-06-11) 67 | 68 | - Added support for multiple groups 69 | - Added `nil` option 70 | 71 | ## 0.0.3 (2014-06-11) 72 | 73 | - Fixed escaping 74 | 75 | ## 0.0.2 (2014-05-29) 76 | 77 | - Added `limit` parameter 78 | 79 | ## 0.0.1 (2014-05-11) 80 | 81 | - First release 82 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Hightop 2 | 3 | A nice shortcut for group count queries 4 | 5 | ```ruby 6 | Visit.top(:browser) 7 | # { 8 | # "Chrome" => 63, 9 | # "Safari" => 50, 10 | # "Firefox" => 34 11 | # } 12 | ``` 13 | 14 | Works with Active Record, Mongoid, arrays and hashes 15 | 16 | [![Build Status](https://github.com/ankane/hightop/actions/workflows/build.yml/badge.svg)](https://github.com/ankane/hightop/actions) 17 | 18 | ## Installation 19 | 20 | Add this line to your application’s Gemfile: 21 | 22 | ```ruby 23 | gem "hightop" 24 | ``` 25 | 26 | ## Options 27 | 28 | Limit the results 29 | 30 | ```ruby 31 | Visit.top(:referring_domain, 10) 32 | ``` 33 | 34 | Include nil values 35 | 36 | ```ruby 37 | Visit.top(:search_keyword, nil: true) 38 | ``` 39 | 40 | Works with multiple groups 41 | 42 | ```ruby 43 | Visit.top([:city, :browser]) 44 | ``` 45 | 46 | And expressions 47 | 48 | ```ruby 49 | Visit.top(Arel.sql("LOWER(referring_domain)")) 50 | ``` 51 | 52 | And distinct 53 | 54 | ```ruby 55 | Visit.top(:city, distinct: :user_id) 56 | ``` 57 | 58 | And min count 59 | 60 | ```ruby 61 | Visit.top(:city, min: 10) 62 | ``` 63 | 64 | ## Arrays and Hashes 65 | 66 | Arrays 67 | 68 | ```ruby 69 | ["up", "up", "down"].top 70 | ``` 71 | 72 | Hashes 73 | 74 | ```ruby 75 | {a: "up", b: "up", c: "down"}.top { |k, v| v } 76 | ``` 77 | 78 | Limit the results 79 | 80 | ```ruby 81 | ["up", "up", "down"].top(1) 82 | ``` 83 | 84 | Include nil values 85 | 86 | ```ruby 87 | [nil, nil, "down"].top(nil: true) 88 | ``` 89 | 90 | Min count 91 | 92 | ```ruby 93 | ["up", "up", "down"].top(min: 2) 94 | ``` 95 | 96 | ## History 97 | 98 | View the [changelog](https://github.com/ankane/hightop/blob/master/CHANGELOG.md) 99 | 100 | ## Contributing 101 | 102 | Everyone is encouraged to help improve this project. Here are a few ways you can help: 103 | 104 | - [Report bugs](https://github.com/ankane/hightop/issues) 105 | - Fix bugs and [submit pull requests](https://github.com/ankane/hightop/pulls) 106 | - Write, clarify, or fix documentation 107 | - Suggest or add new features 108 | 109 | To get started with development: 110 | 111 | ```sh 112 | git clone https://github.com/ankane/hightop.git 113 | cd hightop 114 | bundle install 115 | bundle exec rake test 116 | ``` 117 | -------------------------------------------------------------------------------- /test/enumerable_test.rb: -------------------------------------------------------------------------------- 1 | require_relative "test_helper" 2 | 3 | class EnumerableTest < Minitest::Test 4 | def test_array 5 | top = [:a, :b, :b].top 6 | expected = { 7 | b: 2, 8 | a: 1 9 | } 10 | assert_equal expected, top 11 | assert_equal top.keys, expected.keys 12 | end 13 | 14 | def test_hash 15 | top = { 16 | a: "a", 17 | b: "b", 18 | c: "b" 19 | }.top 20 | # same as methods like tally 21 | expected = { 22 | [:a, "a"] => 1, 23 | [:b, "b"] => 1, 24 | [:c, "b"] => 1 25 | } 26 | assert_equal expected, top 27 | assert_equal top.keys, expected.keys 28 | end 29 | 30 | def test_array_block 31 | top = [:a, :b, :b].top { |v| "#{v}!" } 32 | expected = { 33 | "b!" => 2, 34 | "a!" => 1 35 | } 36 | assert_equal expected, top 37 | assert_equal top.keys, expected.keys 38 | end 39 | 40 | def test_hash_block 41 | top = { 42 | a: "a", 43 | b: "b", 44 | c: "b" 45 | }.top { |k, v| "#{v}!" } 46 | expected = { 47 | "b!" => 2, 48 | "a!" => 1 49 | } 50 | assert_equal expected, top 51 | assert_equal top.keys, expected.keys 52 | end 53 | 54 | def test_limit 55 | top = [:a, :b, :b].top(1) 56 | expected = { 57 | b: 2 58 | } 59 | assert_equal expected, top 60 | end 61 | 62 | def test_limit_order 63 | top = [:b, :c].top(1) 64 | expected = { 65 | b: 1 66 | } 67 | assert_equal expected, top 68 | 69 | top = [:c, :b].top(1) 70 | expected = { 71 | c: 1 72 | } 73 | assert_equal expected, top 74 | end 75 | 76 | def test_nil_values 77 | top = [:a, nil, nil].top 78 | expected = { 79 | a: 1 80 | } 81 | assert_equal expected, top 82 | assert_equal top.keys, expected.keys 83 | end 84 | 85 | def test_nil_option 86 | top = [:a, nil, nil].top(nil: true) 87 | expected = { 88 | nil => 2, 89 | a: 1 90 | } 91 | assert_equal expected, top 92 | assert_equal top.keys, expected.keys 93 | end 94 | 95 | def test_multiple_groups 96 | top = [:a, :b, :b].top { |v| [v, :z] } 97 | expected = { 98 | [:b, :z] => 2, 99 | [:a, :z] => 1 100 | } 101 | assert_equal expected, top 102 | end 103 | 104 | def test_min 105 | top = [:a, :b, :b].top(min: 2) 106 | expected = { 107 | b: 2 108 | } 109 | assert_equal expected, top 110 | end 111 | 112 | def test_too_many_arguments 113 | assert_raises(ArgumentError) do 114 | [].top(1, 2) 115 | end 116 | end 117 | end 118 | -------------------------------------------------------------------------------- /test/model_test.rb: -------------------------------------------------------------------------------- 1 | require_relative "test_helper" 2 | 3 | class ModelTest < Minitest::Test 4 | def setup 5 | Visit.delete_all 6 | end 7 | 8 | def test_top 9 | create_city("San Francisco", 3) 10 | create_city("Chicago", 2) 11 | expected = { 12 | "San Francisco" => 3, 13 | "Chicago" => 2 14 | } 15 | assert_equal expected, Visit.top(:city) 16 | end 17 | 18 | def test_limit 19 | create_city("San Francisco", 3) 20 | create_city("Chicago", 2) 21 | create_city("Boston", 1) 22 | expected = { 23 | "San Francisco" => 3, 24 | "Chicago" => 2 25 | } 26 | assert_equal expected, Visit.top(:city, 2) 27 | assert_equal expected, Visit.limit(2).top(:city) unless mongoid? 28 | end 29 | 30 | def test_nil_values 31 | create_city("San Francisco", 3) 32 | create_city(nil, 2) 33 | expected = { 34 | "San Francisco" => 3 35 | } 36 | assert_equal expected, Visit.top(:city) 37 | end 38 | 39 | def test_nil_option 40 | create_city("San Francisco", 3) 41 | create_city(nil, 2) 42 | expected = { 43 | "San Francisco" => 3, 44 | nil => 2 45 | } 46 | assert_equal expected, Visit.top(:city, nil: true) 47 | end 48 | 49 | def test_multiple_groups 50 | create(city: "San Francisco", user_id: "123") 51 | expected = { 52 | ["San Francisco", "123"] => 1 53 | } 54 | assert_equal expected, Visit.top([:city, :user_id]) 55 | end 56 | 57 | def test_expression 58 | create_city("San Francisco") 59 | expected = { 60 | "san francisco" => 1 61 | } 62 | 63 | if mongoid? 64 | assert_equal expected, Visit.all.project(city: {"$toLower" => "$city"}).top(:city) 65 | else 66 | assert_equal expected, Visit.top(Arel.sql("LOWER(city)")) 67 | end 68 | end 69 | 70 | def test_expression_no_arel 71 | skip if mongoid? 72 | 73 | assert_raises(ActiveRecord::UnknownAttributeReference) do 74 | Visit.top("LOWER(city)") 75 | end 76 | end 77 | 78 | def test_expression_multiple 79 | create_city("San Francisco") 80 | expected = { 81 | ["san francisco", "SAN FRANCISCO"] => 1 82 | } 83 | 84 | if mongoid? 85 | assert_equal expected, Visit.all.project(lower_city: {"$toLower" => "$city"}, upper_city: {"$toUpper" => "$city"}).top([:lower_city, :upper_city]) 86 | else 87 | assert_equal expected, Visit.top([Arel.sql("LOWER(city)"), Arel.sql("UPPER(city)")]) 88 | end 89 | end 90 | 91 | def test_expression_multiple_no_arel 92 | skip if mongoid? 93 | 94 | assert_raises(ActiveRecord::UnknownAttributeReference) do 95 | Visit.top([Arel.sql("LOWER(city)"), "UPPER(city)"]) 96 | end 97 | end 98 | 99 | def test_distinct 100 | create(city: "San Francisco", user_id: 1) 101 | create(city: "San Francisco", user_id: 1) 102 | expected = { 103 | "San Francisco" => 1 104 | } 105 | assert_equal expected, Visit.top(:city, distinct: :user_id) 106 | end 107 | 108 | def test_distinct_expression 109 | skip if mongoid? 110 | 111 | create(city: "San Francisco", user_id: 1) 112 | create(city: "San Francisco", user_id: 1) 113 | expected = { 114 | "San Francisco" => 1 115 | } 116 | assert_equal expected, Visit.top(:city, distinct: Arel.sql("(user_id)")) 117 | end 118 | 119 | def test_distinct_expression_no_arel 120 | skip if mongoid? 121 | 122 | assert_raises(ActiveRecord::UnknownAttributeReference) do 123 | Visit.top(:city, distinct: "(user_id)") 124 | end 125 | end 126 | 127 | def test_min 128 | create_city("San Francisco", 3) 129 | create_city("Chicago", 2) 130 | expected = { 131 | "San Francisco" => 3 132 | } 133 | assert_equal expected, Visit.top(:city, min: 3) 134 | end 135 | 136 | def test_min_distinct 137 | create(city: "San Francisco", user_id: 1) 138 | create(city: "San Francisco", user_id: 1) 139 | create(city: "San Francisco", user_id: 2) 140 | create(city: "Chicago", user_id: 1) 141 | create(city: "Chicago", user_id: 1) 142 | expected = { 143 | "San Francisco" => 2 144 | } 145 | assert_equal expected, Visit.top(:city, min: 2, distinct: :user_id) 146 | end 147 | 148 | def test_where 149 | create_city("San Francisco") 150 | create_city("Chicago") 151 | expected = { 152 | "San Francisco" => 1 153 | } 154 | assert_equal expected, Visit.where(city: "San Francisco").top(:city) 155 | end 156 | 157 | def test_where_options 158 | create(city: "San Francisco", user_id: 1) 159 | create(city: "San Francisco", user_id: 1) 160 | create(city: "San Francisco", user_id: 2) 161 | create(city: "Chicago", user_id: 1) 162 | expected = { 163 | "San Francisco" => 2 164 | } 165 | assert_equal expected, Visit.where(city: "San Francisco").top(:city, distinct: :user_id) 166 | end 167 | 168 | def test_relation_block 169 | create_city("San Francisco", 3) 170 | create_city("Chicago", 2) 171 | expected = { 172 | "San Francisco" => 3, 173 | "Chicago" => 2 174 | } 175 | assert_equal expected, Visit.all.top { |v| v.city } 176 | 177 | # TODO maybe support 178 | # assert_equal expected, Visit.top { |v| v.city } 179 | end 180 | 181 | def test_bad_argument 182 | assert_raises(ArgumentError) do 183 | Visit.top(:city, boom: true) 184 | end 185 | end 186 | 187 | def test_connection_leasing 188 | skip if mongoid? 189 | 190 | ActiveRecord::Base.connection_handler.clear_active_connections! 191 | assert_nil ActiveRecord::Base.connection_pool.active_connection? 192 | ActiveRecord::Base.connection_pool.with_connection do 193 | Visit.top(:city) 194 | end 195 | assert_nil ActiveRecord::Base.connection_pool.active_connection? 196 | end 197 | 198 | private 199 | 200 | def create_city(city, count = 1) 201 | create({city: city}, count) 202 | end 203 | 204 | def create(attributes, count = 1) 205 | count.times { Visit.create!(attributes) } 206 | end 207 | 208 | def mongoid? 209 | defined?(Mongoid) 210 | end 211 | end 212 | --------------------------------------------------------------------------------