activerecord-has_count 0.0.1 → 0.0.2

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
  SHA1:
3
- metadata.gz: 1638ba180bf14cd976c29912e38505fb21b73a4b
4
- data.tar.gz: dd52048a4744eb4f5b740b0b0df9ca74f8a8eb99
3
+ metadata.gz: 69e774d016a1d4b507eb55f8f0f9504cbf198a3b
4
+ data.tar.gz: abbd2d57baa8ebdf82d65ce8f6080febafaaaf57
5
5
  SHA512:
6
- metadata.gz: c3e3313087b1e78e6ef21b26c938a4ebe17124c74ac9c596970f4614e329f7f89375f1bdf3c2586b6dcd068f0e3ed705ccb38526ddb8b2d59e7635d6e8179930
7
- data.tar.gz: 8128b246278b8c678a7fd8a52588e570444555e8e5362a0636cdb86170b77e92c8e2a3695035d393daa485ef7aad6d812ef62ddd923c64ce8065f1d9500bbcec
6
+ metadata.gz: 7ef3dacda31025b0d12def2faa3b14a75b43ab114a0389489a7962bc0e1c53542402991e0809198192ba8c28cc77c0bfd56c7533f79ca05a5cc6fe0406c65b94
7
+ data.tar.gz: 30460a520bfd15c0f131ec19091415976834fb0b876bc4c362148bc604f34a71827b39e76e33f00d1f07d03f8e4148e1e480191829e007ad4daad26dad74a73c
data/.gitignore CHANGED
@@ -15,3 +15,4 @@ spec/reports
15
15
  test/tmp
16
16
  test/version_tmp
17
17
  tmp
18
+ *.sqlite3
data/.rspec ADDED
@@ -0,0 +1 @@
1
+ -c
data/.travis.yml ADDED
@@ -0,0 +1,7 @@
1
+ rvm:
2
+ - 2.0.0
3
+ branches:
4
+ only:
5
+ - master
6
+ script:
7
+ - "bundle exec rspec"
data/README.md CHANGED
@@ -1,4 +1,4 @@
1
- # ActiveRecord::HasCount
1
+ # ActiveRecord::HasCount [![Build Status](https://travis-ci.org/k0kubun/activerecord-has_count.png?branch=master)](https://travis-ci.org/k0kubun/activerecord-has_count)
2
2
 
3
3
  N+1 count query killer for ActiveRecord
4
4
  ActiveRecord::HasCount allows you to cache count of associated records by eager loading
@@ -20,7 +20,7 @@ gem 'activerecord-has_count'
20
20
 
21
21
  ## Usage
22
22
 
23
- ### Add count\_preloadable scope
23
+ ### Add has\_count scope
24
24
  First, call `has_count` with an association whose count you want to preload
25
25
 
26
26
  ```rb
@@ -63,7 +63,7 @@ end
63
63
 
64
64
  ## Contributing
65
65
 
66
- 1. Fork it ( https://github.com/k0kubun/has_count/fork )
66
+ 1. Fork it ( https://github.com/k0kubun/activerecord-has_count/fork )
67
67
  2. Create your feature branch (`git checkout -b my-new-feature`)
68
68
  3. Commit your changes (`git commit -am 'Add some feature'`)
69
69
  4. Push to the branch (`git push origin my-new-feature`)
@@ -1,6 +1,6 @@
1
1
  lib = File.expand_path('../lib', __FILE__)
2
2
  $LOAD_PATH.unshift(lib) unless $LOAD_PATH.include?(lib)
3
- require 'active_record/has_count/version'
3
+ require 'activerecord-has_count/version'
4
4
 
5
5
  Gem::Specification.new do |spec|
6
6
  spec.name = "activerecord-has_count"
@@ -17,7 +17,7 @@ Gem::Specification.new do |spec|
17
17
  spec.require_paths = ["lib"]
18
18
 
19
19
  spec.required_ruby_version = ">= 1.9.2"
20
- spec.add_runtime_dependency "activerecord", ">= 3.0"
20
+ spec.add_runtime_dependency "activerecord", ">= 3.2.0"
21
21
  spec.add_development_dependency "rspec", "~> 3.0.0"
22
22
  spec.add_development_dependency "factory_girl", "~> 4.2.0"
23
23
  spec.add_development_dependency "sqlite3"
@@ -0,0 +1,9 @@
1
+ source 'https://rubygems.org'
2
+
3
+ gem_dir = File.expand_path("../../", __FILE__)
4
+
5
+ gem "activerecord", "~> 4.0"
6
+ gem "activerecord-import"
7
+ gem "activerecord-has_count", path: gem_dir
8
+ gem "pry"
9
+ gem "mysql2"
@@ -0,0 +1,16 @@
1
+ # N+1 COUNT query benchmarks
2
+
3
+ ## Preparation
4
+
5
+ ```bash
6
+ $ git clone git@github.com:k0kubun/activerecord-has_count
7
+ $ cd activerecord-has_count
8
+ $ mysql -uroot -e"create database bench"
9
+ $ bundle install
10
+ ```
11
+
12
+ ## Run
13
+
14
+ ```bash
15
+ $ bundle exec ruby benchmark.rb
16
+ ```
@@ -0,0 +1,60 @@
1
+ require "benchmark"
2
+ require "active_record"
3
+ require "activerecord-import"
4
+ require "activerecord-has_count"
5
+
6
+ spec_dir = File.expand_path("../../spec", __FILE__)
7
+ Dir[File.join(spec_dir, "models/*.rb")].each { |f| require f }
8
+
9
+ database_yml = File.join(spec_dir, "database.yml")
10
+ ActiveRecord::Base.configurations["bench"] = YAML.load_file(database_yml)["bench"]
11
+ ActiveRecord::Base.establish_connection :bench
12
+
13
+ ActiveRecord::Schema.define do
14
+ create_table :tweets, force: true do |t|
15
+ t.column :created_at, :datetime
16
+ t.column :updated_at, :datetime
17
+ end
18
+
19
+ create_table :replies, force: true do |t|
20
+ t.column :tweet_id, :integer
21
+ t.column :created_at, :datetime
22
+ t.column :updated_at, :datetime
23
+ end
24
+ end
25
+
26
+ [Tweet, Reply].each(&:delete_all)
27
+
28
+ TWEET_COUNT = 5
29
+ REPLY_COUNT = 10000
30
+
31
+ TWEET_COUNT.times do
32
+ tweet = Tweet.create
33
+
34
+ replies = REPLY_COUNT.times.map do
35
+ Reply.new(tweet: tweet)
36
+ end
37
+ Reply.import(replies, validate: false)
38
+ end
39
+
40
+ Benchmark.bmbm do |bench|
41
+ bench.report("COUNT association") do
42
+ tweets = Tweet.first(TWEET_COUNT)
43
+
44
+ tweets.each { |t| t.replies.count }
45
+ end
46
+
47
+ bench.report("LEFT JOIN") do
48
+ tweets = Tweet.joins('LEFT JOIN replies ON tweets.id = replies.tweet_id').
49
+ select('tweets.*, COUNT(replies.id) AS replies_count').
50
+ group('tweets.id').first(TWEET_COUNT)
51
+
52
+ tweets.each { |t| t.replies_count }
53
+ end
54
+
55
+ bench.report("size of preloaded association") do
56
+ tweets = Tweet.preload(:replies).first(TWEET_COUNT)
57
+
58
+ tweets.each { |t| t.replies.size }
59
+ end
60
+ end
@@ -1,20 +1,7 @@
1
1
  module ActiveRecord
2
2
  module Associations
3
- module ClassMethods
4
- module HasCount
5
- private
6
-
7
- def has_count(name, scope = nil, options = {}, &extension)
8
- name_with_count = :"#{name}_count"
9
-
10
- reflection = Builder::HasCount.build(self, name_with_count, scope, options, &extension)
11
- Reflection.add_reflection(self, name_with_count, reflection)
12
- end
13
- end
14
- end
15
-
16
3
  class HasCount < SingularAssociation
17
- # Not preloaded behaviour of count_preloadable association
4
+ # Not preloaded behaviour of has_count association
18
5
  # When this method is called, it will be N+1 query
19
6
  def load_target
20
7
  count_target = name_without_count.to_sym
@@ -0,0 +1,23 @@
1
+ module ActiveRecord
2
+ module HasCount
3
+ module Model
4
+ def self.included(model)
5
+ model.singleton_class.class_eval do
6
+ include ClassMethods
7
+ end
8
+ end
9
+
10
+ module ClassMethods
11
+ private
12
+
13
+ def has_count(name, scope = nil, options = {}, &extension)
14
+ name_with_count = :"#{name}_count"
15
+
16
+ reflection = ActiveRecord::Associations::Builder::HasCount.
17
+ build(self, name_with_count, scope, options, &extension)
18
+ ActiveRecord::Reflection.add_reflection(self, name_with_count, reflection)
19
+ end
20
+ end
21
+ end
22
+ end
23
+ end
@@ -1,5 +1,5 @@
1
1
  module ActiveRecord
2
2
  module HasCount
3
- VERSION = "0.0.1"
3
+ VERSION = "0.0.2"
4
4
  end
5
5
  end
@@ -1,15 +1,15 @@
1
+ require "active_record"
2
+ require "active_support/lazy_load_hooks"
3
+
1
4
  require "active_record/associations/has_count"
2
5
  require "active_record/associations/builder/has_count"
3
6
  require "active_record/associations/preloader/has_count"
4
7
  require "active_record/associations/join_dependency/has_count"
5
8
  require "active_record/reflection/has_count"
6
9
 
7
- module ActiveRecord
8
- module Associations
9
- module ClassMethods
10
- include HasCount
11
- end
12
- end
10
+ ActiveSupport.on_load(:active_record) do
11
+ require "activerecord-has_count/model"
12
+ ActiveRecord::Base.send(:include, ActiveRecord::HasCount::Model)
13
13
  end
14
14
 
15
15
  module ActiveRecord
data/spec/database.yml ADDED
@@ -0,0 +1,13 @@
1
+ test:
2
+ adapter: sqlite3
3
+ database: test.sqlite3
4
+ pool: 5
5
+ timeout: 5000
6
+ bench:
7
+ adapter: mysql2
8
+ encoding: utf8
9
+ database: bench
10
+ pool: 5
11
+ username: root
12
+ password:
13
+ socket: /tmp/mysql.sock
@@ -0,0 +1,28 @@
1
+ require "spec_helper"
2
+
3
+ describe "#eager_load" do
4
+ describe "has_count association" do
5
+ let(:tweets_count) { 3 }
6
+ let(:tweets) do
7
+ tweets_count.times.map do
8
+ FactoryGirl.create(:tweet)
9
+ end
10
+ end
11
+
12
+ before do
13
+ tweets.each_with_index do |tweet, index|
14
+ index.times do
15
+ FactoryGirl.create(:reply, tweet: tweet)
16
+ end
17
+ end
18
+ end
19
+
20
+ it "raises ActiveRecord::EagerLoadHasCountError" do
21
+ # Currently this is not supported because I can't come up with how to implement.
22
+ # I'm waiting for your contribution.
23
+ expect {
24
+ Tweet.all.eager_load(:replies_count).map(&:replies_count)
25
+ }.to raise_error(ActiveRecord::EagerLoadHasCountError)
26
+ end
27
+ end
28
+ end
@@ -0,0 +1,57 @@
1
+ require "spec_helper"
2
+
3
+ describe "#includes" do
4
+ describe "builtin associations" do
5
+ let(:replies_count) { 3 }
6
+ let!(:tweet) { FactoryGirl.create(:tweet) }
7
+ before do
8
+ replies_count.times do
9
+ FactoryGirl.create(:reply, tweet: tweet)
10
+ end
11
+ end
12
+
13
+ context "given has_many association" do
14
+ it "works as usual" do
15
+ tweet = Tweet.includes(:replies).first
16
+ expect(tweet.replies.count).to eq(replies_count)
17
+ end
18
+ end
19
+
20
+ context "given belongs_to association" do
21
+ let!(:reply) { FactoryGirl.create(:reply, tweet: tweet) }
22
+
23
+ it "works as usual" do
24
+ included_reply = Reply.includes(:tweet).find(reply.id)
25
+ expect(included_reply.tweet).to eq(tweet)
26
+ end
27
+ end
28
+ end
29
+
30
+ describe "has_count association" do
31
+ let(:tweets_count) { 3 }
32
+ let(:tweets) do
33
+ tweets_count.times.map do
34
+ FactoryGirl.create(:tweet)
35
+ end
36
+ end
37
+
38
+ before do
39
+ tweets.each_with_index do |tweet, index|
40
+ index.times do
41
+ FactoryGirl.create(:reply, tweet: tweet)
42
+ end
43
+ end
44
+ end
45
+
46
+ it "does not execute N+1 queries by preload" do
47
+ expect_query_counts(1 + tweets_count) { Tweet.all.map(&:replies_count) }
48
+ expect_query_counts(2) { Tweet.all.includes(:replies_count).map(&:replies_count) }
49
+ end
50
+
51
+ it "counts properly" do
52
+ expected = Tweet.all.map { |t| t.replies.count }
53
+ expect(Tweet.all.map(&:replies_count)).to eq(expected)
54
+ expect(Tweet.all.includes(:replies_count).map(&:replies_count)).to eq(expected)
55
+ end
56
+ end
57
+ end
@@ -0,0 +1,3 @@
1
+ class Reply < ActiveRecord::Base
2
+ belongs_to :tweet
3
+ end
@@ -0,0 +1,4 @@
1
+ class Tweet < ActiveRecord::Base
2
+ has_many :replies
3
+ has_count :replies
4
+ end
@@ -0,0 +1,57 @@
1
+ require "spec_helper"
2
+
3
+ describe "#preload" do
4
+ describe "builtin associations" do
5
+ let(:replies_count) { 3 }
6
+ let!(:tweet) { FactoryGirl.create(:tweet) }
7
+ before do
8
+ replies_count.times do
9
+ FactoryGirl.create(:reply, tweet: tweet)
10
+ end
11
+ end
12
+
13
+ context "given has_many association" do
14
+ it "works as usual" do
15
+ tweet = Tweet.preload(:replies).first
16
+ expect(tweet.replies.count).to eq(replies_count)
17
+ end
18
+ end
19
+
20
+ context "given belongs_to association" do
21
+ let!(:reply) { FactoryGirl.create(:reply, tweet: tweet) }
22
+
23
+ it "works as usual" do
24
+ preloaded_reply = Reply.preload(:tweet).find(reply.id)
25
+ expect(preloaded_reply.tweet).to eq(tweet)
26
+ end
27
+ end
28
+ end
29
+
30
+ describe "has_count association" do
31
+ let(:tweets_count) { 3 }
32
+ let(:tweets) do
33
+ tweets_count.times.map do
34
+ FactoryGirl.create(:tweet)
35
+ end
36
+ end
37
+
38
+ before do
39
+ tweets.each_with_index do |tweet, index|
40
+ index.times do
41
+ FactoryGirl.create(:reply, tweet: tweet)
42
+ end
43
+ end
44
+ end
45
+
46
+ it "does not execute N+1 queries by preload" do
47
+ expect_query_counts(1 + tweets_count) { Tweet.all.map(&:replies_count) }
48
+ expect_query_counts(2) { Tweet.all.preload(:replies_count).map(&:replies_count) }
49
+ end
50
+
51
+ it "counts properly" do
52
+ expected = Tweet.all.map { |t| t.replies.count }
53
+ expect(Tweet.all.map(&:replies_count)).to eq(expected)
54
+ expect(Tweet.all.preload(:replies_count).map(&:replies_count)).to eq(expected)
55
+ end
56
+ end
57
+ end
@@ -0,0 +1,39 @@
1
+ require "pathname"
2
+ require "pry"
3
+ require "active_record"
4
+ require "activerecord-has_count"
5
+
6
+ spec_dir = Pathname.new(File.dirname(__FILE__))
7
+ Dir[File.join(spec_dir, "models/*.rb")].each { |f| require f }
8
+
9
+ require "factory_girl"
10
+ Dir[File.join(spec_dir, "support/*.rb")].each { |f| require f }
11
+
12
+ database_yml = File.join(spec_dir, "database.yml")
13
+ ActiveRecord::Base.configurations["test"] = YAML.load_file(database_yml)["test"]
14
+ ActiveRecord::Base.establish_connection :test
15
+
16
+ ActiveRecord::Schema.define do
17
+ create_table :tweets, force: true do |t|
18
+ t.column :user_id, :integer
19
+ t.column :created_at, :datetime
20
+ t.column :updated_at, :datetime
21
+ end
22
+
23
+ create_table :replies, force: true do |t|
24
+ t.column :tweet_id, :integer
25
+ t.column :created_at, :datetime
26
+ t.column :updated_at, :datetime
27
+ end
28
+ end
29
+
30
+ RSpec.configure do |config|
31
+ ALL_MODELS = [
32
+ Tweet,
33
+ Reply,
34
+ ].freeze
35
+
36
+ config.after do
37
+ ALL_MODELS.each(&:delete_all)
38
+ end
39
+ end
@@ -0,0 +1,54 @@
1
+ # Copied from:
2
+ # https://github.com/rails/rails/blob/9bb76261d39b59e7e229c80d052ca91a65ff17be/activerecord/test/cases/test_case.rb#L40-L52
3
+ def expect_query_counts(num = 1, options = {})
4
+ ignore_none = options.fetch(:ignore_none) { num == :any }
5
+ SQLCounter.clear_log
6
+ x = yield
7
+
8
+ the_log = ignore_none ? SQLCounter.log_all : SQLCounter.log
9
+ expect(the_log.size).to eq(num)
10
+
11
+ x
12
+ end
13
+
14
+ class SQLCounter
15
+ class << self
16
+ attr_accessor :ignored_sql, :log, :log_all
17
+ def clear_log; self.log = []; self.log_all = []; end
18
+ end
19
+
20
+ self.clear_log
21
+
22
+ self.ignored_sql = [/^PRAGMA/, /^SELECT currval/, /^SELECT CAST/, /^SELECT @@IDENTITY/, /^SELECT @@ROWCOUNT/, /^SAVEPOINT/, /^ROLLBACK TO SAVEPOINT/, /^RELEASE SAVEPOINT/, /^SHOW max_identifier_length/, /^BEGIN/, /^COMMIT/]
23
+
24
+ # FIXME: this needs to be refactored so specific database can add their own
25
+ # ignored SQL, or better yet, use a different notification for the queries
26
+ # instead examining the SQL content.
27
+ oracle_ignored = [/^select .*nextval/i, /^SAVEPOINT/, /^ROLLBACK TO/, /^\s*select .* from all_triggers/im, /^\s*select .* from all_constraints/im, /^\s*select .* from all_tab_cols/im]
28
+ mysql_ignored = [/^SHOW TABLES/i, /^SHOW FULL FIELDS/, /^SHOW CREATE TABLE /i]
29
+ postgresql_ignored = [/^\s*select\b.*\bfrom\b.*pg_namespace\b/im, /^\s*select\b.*\battname\b.*\bfrom\b.*\bpg_attribute\b/im, /^SHOW search_path/i]
30
+ sqlite3_ignored = [/^\s*SELECT name\b.*\bFROM sqlite_master/im]
31
+
32
+ [oracle_ignored, mysql_ignored, postgresql_ignored, sqlite3_ignored].each do |db_ignored_sql|
33
+ ignored_sql.concat db_ignored_sql
34
+ end
35
+
36
+ attr_reader :ignore
37
+
38
+ def initialize(ignore = Regexp.union(self.class.ignored_sql))
39
+ @ignore = ignore
40
+ end
41
+
42
+ def call(name, start, finish, message_id, values)
43
+ sql = values[:sql]
44
+
45
+ # FIXME: this seems bad. we should probably have a better way to indicate
46
+ # the query was cached
47
+ return if 'CACHE' == values[:name]
48
+
49
+ self.class.log_all << sql
50
+ self.class.log << sql unless ignore =~ sql
51
+ end
52
+ end
53
+
54
+ ActiveSupport::Notifications.subscribe('sql.active_record', SQLCounter.new)
@@ -0,0 +1,4 @@
1
+ FactoryGirl.define do
2
+ factory :tweet
3
+ factory :reply
4
+ end
metadata CHANGED
@@ -1,14 +1,14 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: activerecord-has_count
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.0.1
4
+ version: 0.0.2
5
5
  platform: ruby
6
6
  authors:
7
7
  - Takashi Kokubun
8
8
  autorequire:
9
9
  bindir: bin
10
10
  cert_chain: []
11
- date: 2014-09-13 00:00:00.000000000 Z
11
+ date: 2014-09-16 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: activerecord
@@ -16,14 +16,14 @@ dependencies:
16
16
  requirements:
17
17
  - - '>='
18
18
  - !ruby/object:Gem::Version
19
- version: '3.0'
19
+ version: 3.2.0
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: '3.0'
26
+ version: 3.2.0
27
27
  - !ruby/object:Gem::Dependency
28
28
  name: rspec
29
29
  requirement: !ruby/object:Gem::Requirement
@@ -116,18 +116,33 @@ extensions: []
116
116
  extra_rdoc_files: []
117
117
  files:
118
118
  - .gitignore
119
+ - .rspec
120
+ - .travis.yml
119
121
  - Gemfile
120
122
  - LICENSE.txt
121
123
  - README.md
122
124
  - Rakefile
123
125
  - activerecord-has_count.gemspec
126
+ - benchmarks/Gemfile
127
+ - benchmarks/README.md
128
+ - benchmarks/benchmark.rb
124
129
  - lib/active_record/associations/builder/has_count.rb
125
130
  - lib/active_record/associations/has_count.rb
126
131
  - lib/active_record/associations/join_dependency/has_count.rb
127
132
  - lib/active_record/associations/preloader/has_count.rb
128
- - lib/active_record/has_count/version.rb
129
133
  - lib/active_record/reflection/has_count.rb
130
134
  - lib/activerecord-has_count.rb
135
+ - lib/activerecord-has_count/model.rb
136
+ - lib/activerecord-has_count/version.rb
137
+ - spec/database.yml
138
+ - spec/eager_load_spec.rb
139
+ - spec/includes_spec.rb
140
+ - spec/models/reply.rb
141
+ - spec/models/tweet.rb
142
+ - spec/preload_spec.rb
143
+ - spec/spec_helper.rb
144
+ - spec/support/assert_queries.rb
145
+ - spec/support/factories.rb
131
146
  homepage: https://github.com/k0kubun/activerecord-has_count
132
147
  licenses:
133
148
  - MIT
@@ -148,9 +163,18 @@ required_rubygems_version: !ruby/object:Gem::Requirement
148
163
  version: '0'
149
164
  requirements: []
150
165
  rubyforge_project:
151
- rubygems_version: 2.0.3
166
+ rubygems_version: 2.0.14
152
167
  signing_key:
153
168
  specification_version: 4
154
169
  summary: N+1 count query killer for ActiveRecord
155
- test_files: []
170
+ test_files:
171
+ - spec/database.yml
172
+ - spec/eager_load_spec.rb
173
+ - spec/includes_spec.rb
174
+ - spec/models/reply.rb
175
+ - spec/models/tweet.rb
176
+ - spec/preload_spec.rb
177
+ - spec/spec_helper.rb
178
+ - spec/support/assert_queries.rb
179
+ - spec/support/factories.rb
156
180
  has_rdoc: