predictive_load 0.1.2 → 0.2.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 +4 -4
- data/README.md +21 -3
- data/lib/predictive_load.rb +2 -0
- data/lib/predictive_load/loader.rb +17 -11
- data/lib/predictive_load/preload_log.rb +4 -2
- data/lib/predictive_load/watcher.rb +2 -0
- metadata +125 -26
- data/.gitignore +0 -2
- data/.travis.yml +0 -4
- data/Gemfile +0 -7
- data/Rakefile +0 -11
- data/predictive_load.gemspec +0 -16
- data/test/active_record_collection_observation_test.rb +0 -44
- data/test/database.yml +0 -3
- data/test/helper.rb +0 -57
- data/test/loader_test.rb +0 -155
- data/test/models.rb +0 -36
- data/test/schema.rb +0 -37
- data/test/watcher_test.rb +0 -96
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA1:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: eb9fa507002032502359805bb5a3e97730abba37
|
4
|
+
data.tar.gz: 40ad5bd0b8d8aa0b65bdd4d81bd20617a901744f
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: 785eaf1a3a9a1ad2f7c79ad0603ae92e0366cde058ffe8941ab5e81093df731562f6c90b8724aa5f0613949058c7aa0bbf414992ea8b4d597524fee79b024d70
|
7
|
+
data.tar.gz: 3ac672859b89465d53fe8086dad7d82fe3519869452537ac7ab551b7c7216111a1b336c4f746aebb2c9af6e720d01b3e40a680e3b1a575406cf313ca38412d44
|
data/README.md
CHANGED
@@ -1,4 +1,4 @@
|
|
1
|
-
[](https://travis-ci.org/eac/predictive_load)
|
2
2
|
|
3
3
|
predictive_load
|
4
4
|
===============
|
@@ -7,12 +7,16 @@ Observes Active Record collections and notifies when a member loads an associati
|
|
7
7
|
* automatically preloading the association in a single query for all members of that collection.
|
8
8
|
* N+1 detection logging
|
9
9
|
|
10
|
-
|
11
|
-
|
12
10
|
### Automatic preloading
|
13
11
|
|
14
12
|
|
15
13
|
```ruby
|
14
|
+
require 'predictive_load'
|
15
|
+
require 'predictive_load/active_record_collection_observation'
|
16
|
+
ActiveRecord::Base.send(:include, PredictiveLoad::ActiveRecordCollectionObservation)
|
17
|
+
|
18
|
+
require 'predictive_load/loader'
|
19
|
+
|
16
20
|
ActiveRecord::Relation.collection_observer = PredictiveLoad::Loader
|
17
21
|
|
18
22
|
Ticket.all.each do |ticket|
|
@@ -28,10 +32,24 @@ Produces:
|
|
28
32
|
SELECT `accounts`.* FROM `accounts` WHERE `accounts`.`id` IN (1, 2, 3)
|
29
33
|
```
|
30
34
|
|
35
|
+
### Disabling preload
|
36
|
+
|
37
|
+
Some things cannot be preloaded, use `no_preload`
|
38
|
+
|
39
|
+
```
|
40
|
+
has_many :foos, no_preload: true
|
41
|
+
```
|
42
|
+
|
31
43
|
### N+1 detection logging
|
32
44
|
|
33
45
|
There is also a log-only version:
|
34
46
|
```ruby
|
47
|
+
require 'predictive_load'
|
48
|
+
require 'predictive_load/active_record_collection_observation'
|
49
|
+
ActiveRecord::Base.send(:include, PredictiveLoad::ActiveRecordCollectionObservation)
|
50
|
+
|
51
|
+
require 'predictive_load/watcher'
|
52
|
+
|
35
53
|
ActiveRecord::Relation.collection_observer = PredictiveLoad::Watcher
|
36
54
|
|
37
55
|
Comment.all.each do |comment|
|
data/lib/predictive_load.rb
CHANGED
@@ -5,7 +5,6 @@ module PredictiveLoad
|
|
5
5
|
# ActiveRecord::Relation.collection_observer = LazyLoader
|
6
6
|
#
|
7
7
|
class Loader
|
8
|
-
|
9
8
|
def self.observe(records)
|
10
9
|
new(records).observe
|
11
10
|
end
|
@@ -28,6 +27,10 @@ module PredictiveLoad
|
|
28
27
|
end
|
29
28
|
end
|
30
29
|
|
30
|
+
protected
|
31
|
+
|
32
|
+
attr_reader :records
|
33
|
+
|
31
34
|
def all_records_will_likely_load_association?(association_name)
|
32
35
|
if defined?(Mocha) && association_name.to_s.index('_stub_')
|
33
36
|
false
|
@@ -37,16 +40,20 @@ module PredictiveLoad
|
|
37
40
|
end
|
38
41
|
|
39
42
|
def supports_preload?(association)
|
40
|
-
return false if association.reflection.options[:
|
43
|
+
return false if association.reflection.options[:no_preload]
|
44
|
+
return false if association.reflection.options[:conditions].respond_to?(:to_proc) # rails 3 conditions proc (we do not know if it uses instance methods)
|
45
|
+
if ActiveRecord::VERSION::MAJOR > 3
|
46
|
+
return false if association.reflection.scope.try(:arity).to_i > 0 # rails 4+ conditions block, if it uses a passed in object, we assume it is not preloadable
|
47
|
+
end
|
41
48
|
true
|
42
49
|
end
|
43
50
|
|
44
|
-
protected
|
45
|
-
|
46
|
-
attr_reader :records
|
47
|
-
|
48
51
|
def preload(association_name)
|
49
|
-
ActiveRecord::
|
52
|
+
if ActiveRecord::VERSION::STRING <= "4.1.0"
|
53
|
+
ActiveRecord::Associations::Preloader.new(records_with_association(association_name), [ association_name ]).run
|
54
|
+
else
|
55
|
+
ActiveRecord::Associations::Preloader.new.preload(records_with_association(association_name), [ association_name ])
|
56
|
+
end
|
50
57
|
end
|
51
58
|
|
52
59
|
def records_with_association(association_name)
|
@@ -59,10 +66,9 @@ module PredictiveLoad
|
|
59
66
|
|
60
67
|
def mixed_collection?
|
61
68
|
@mixed_collection ||= begin
|
62
|
-
|
63
|
-
|
64
|
-
|
69
|
+
klass = records.first.class
|
70
|
+
records.any? { |record| record.class != klass }
|
71
|
+
end
|
65
72
|
end
|
66
|
-
|
67
73
|
end
|
68
74
|
end
|
@@ -8,14 +8,16 @@ module PredictiveLoad
|
|
8
8
|
def preload(association)
|
9
9
|
grouped_records(association).each do |reflection, klasses|
|
10
10
|
klasses.each do |klass, records|
|
11
|
-
|
11
|
+
preload_scope = (ActiveRecord::VERSION::MAJOR == 3 ? options : self.preload_scope)
|
12
|
+
preloader = preloader_for(reflection).new(klass, records, reflection, preload_scope)
|
12
13
|
|
13
14
|
if preloader.respond_to?(:through_reflection)
|
14
15
|
log("encountered :through association for #{association}. Requires loading records to generate query, so skipping for now.")
|
15
16
|
next
|
16
17
|
end
|
17
18
|
|
18
|
-
|
19
|
+
scope = (ActiveRecord::VERSION::MAJOR == 3 ? preloader.scoped : preloader.scope)
|
20
|
+
preload_sql = scope.where(collection_arel(preloader)).to_sql
|
19
21
|
|
20
22
|
log("would preload with: #{preload_sql.to_s}")
|
21
23
|
klass.connection.explain(preload_sql).each_line do |line|
|
metadata
CHANGED
@@ -1,15 +1,133 @@
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
2
2
|
name: predictive_load
|
3
3
|
version: !ruby/object:Gem::Version
|
4
|
-
version: 0.
|
4
|
+
version: 0.2.0
|
5
5
|
platform: ruby
|
6
6
|
authors:
|
7
7
|
- Eric Chapweske
|
8
8
|
autorequire:
|
9
9
|
bindir: bin
|
10
10
|
cert_chain: []
|
11
|
-
date:
|
12
|
-
dependencies:
|
11
|
+
date: 2015-09-02 00:00:00.000000000 Z
|
12
|
+
dependencies:
|
13
|
+
- !ruby/object:Gem::Dependency
|
14
|
+
name: activerecord
|
15
|
+
requirement: !ruby/object:Gem::Requirement
|
16
|
+
requirements:
|
17
|
+
- - ">="
|
18
|
+
- !ruby/object:Gem::Version
|
19
|
+
version: 3.2.0
|
20
|
+
- - "<"
|
21
|
+
- !ruby/object:Gem::Version
|
22
|
+
version: 4.3.0
|
23
|
+
type: :runtime
|
24
|
+
prerelease: false
|
25
|
+
version_requirements: !ruby/object:Gem::Requirement
|
26
|
+
requirements:
|
27
|
+
- - ">="
|
28
|
+
- !ruby/object:Gem::Version
|
29
|
+
version: 3.2.0
|
30
|
+
- - "<"
|
31
|
+
- !ruby/object:Gem::Version
|
32
|
+
version: 4.3.0
|
33
|
+
- !ruby/object:Gem::Dependency
|
34
|
+
name: minitest
|
35
|
+
requirement: !ruby/object:Gem::Requirement
|
36
|
+
requirements:
|
37
|
+
- - ">="
|
38
|
+
- !ruby/object:Gem::Version
|
39
|
+
version: '0'
|
40
|
+
type: :development
|
41
|
+
prerelease: false
|
42
|
+
version_requirements: !ruby/object:Gem::Requirement
|
43
|
+
requirements:
|
44
|
+
- - ">="
|
45
|
+
- !ruby/object:Gem::Version
|
46
|
+
version: '0'
|
47
|
+
- !ruby/object:Gem::Dependency
|
48
|
+
name: minitest-rg
|
49
|
+
requirement: !ruby/object:Gem::Requirement
|
50
|
+
requirements:
|
51
|
+
- - ">="
|
52
|
+
- !ruby/object:Gem::Version
|
53
|
+
version: '0'
|
54
|
+
type: :development
|
55
|
+
prerelease: false
|
56
|
+
version_requirements: !ruby/object:Gem::Requirement
|
57
|
+
requirements:
|
58
|
+
- - ">="
|
59
|
+
- !ruby/object:Gem::Version
|
60
|
+
version: '0'
|
61
|
+
- !ruby/object:Gem::Dependency
|
62
|
+
name: sqlite3
|
63
|
+
requirement: !ruby/object:Gem::Requirement
|
64
|
+
requirements:
|
65
|
+
- - ">="
|
66
|
+
- !ruby/object:Gem::Version
|
67
|
+
version: '0'
|
68
|
+
type: :development
|
69
|
+
prerelease: false
|
70
|
+
version_requirements: !ruby/object:Gem::Requirement
|
71
|
+
requirements:
|
72
|
+
- - ">="
|
73
|
+
- !ruby/object:Gem::Version
|
74
|
+
version: '0'
|
75
|
+
- !ruby/object:Gem::Dependency
|
76
|
+
name: rake
|
77
|
+
requirement: !ruby/object:Gem::Requirement
|
78
|
+
requirements:
|
79
|
+
- - ">="
|
80
|
+
- !ruby/object:Gem::Version
|
81
|
+
version: '0'
|
82
|
+
type: :development
|
83
|
+
prerelease: false
|
84
|
+
version_requirements: !ruby/object:Gem::Requirement
|
85
|
+
requirements:
|
86
|
+
- - ">="
|
87
|
+
- !ruby/object:Gem::Version
|
88
|
+
version: '0'
|
89
|
+
- !ruby/object:Gem::Dependency
|
90
|
+
name: bump
|
91
|
+
requirement: !ruby/object:Gem::Requirement
|
92
|
+
requirements:
|
93
|
+
- - ">="
|
94
|
+
- !ruby/object:Gem::Version
|
95
|
+
version: '0'
|
96
|
+
type: :development
|
97
|
+
prerelease: false
|
98
|
+
version_requirements: !ruby/object:Gem::Requirement
|
99
|
+
requirements:
|
100
|
+
- - ">="
|
101
|
+
- !ruby/object:Gem::Version
|
102
|
+
version: '0'
|
103
|
+
- !ruby/object:Gem::Dependency
|
104
|
+
name: wwtd
|
105
|
+
requirement: !ruby/object:Gem::Requirement
|
106
|
+
requirements:
|
107
|
+
- - ">="
|
108
|
+
- !ruby/object:Gem::Version
|
109
|
+
version: '0'
|
110
|
+
type: :development
|
111
|
+
prerelease: false
|
112
|
+
version_requirements: !ruby/object:Gem::Requirement
|
113
|
+
requirements:
|
114
|
+
- - ">="
|
115
|
+
- !ruby/object:Gem::Version
|
116
|
+
version: '0'
|
117
|
+
- !ruby/object:Gem::Dependency
|
118
|
+
name: query_diet
|
119
|
+
requirement: !ruby/object:Gem::Requirement
|
120
|
+
requirements:
|
121
|
+
- - ">="
|
122
|
+
- !ruby/object:Gem::Version
|
123
|
+
version: '0'
|
124
|
+
type: :development
|
125
|
+
prerelease: false
|
126
|
+
version_requirements: !ruby/object:Gem::Requirement
|
127
|
+
requirements:
|
128
|
+
- - ">="
|
129
|
+
- !ruby/object:Gem::Version
|
130
|
+
version: '0'
|
13
131
|
description: Predictive loader
|
14
132
|
email:
|
15
133
|
- eac@zendesk.com
|
@@ -17,25 +135,13 @@ executables: []
|
|
17
135
|
extensions: []
|
18
136
|
extra_rdoc_files: []
|
19
137
|
files:
|
20
|
-
- .gitignore
|
21
|
-
- .travis.yml
|
22
|
-
- Gemfile
|
23
138
|
- LICENSE
|
24
139
|
- README.md
|
25
|
-
- Rakefile
|
26
140
|
- lib/predictive_load.rb
|
27
141
|
- lib/predictive_load/active_record_collection_observation.rb
|
28
142
|
- lib/predictive_load/loader.rb
|
29
143
|
- lib/predictive_load/preload_log.rb
|
30
144
|
- lib/predictive_load/watcher.rb
|
31
|
-
- predictive_load.gemspec
|
32
|
-
- test/active_record_collection_observation_test.rb
|
33
|
-
- test/database.yml
|
34
|
-
- test/helper.rb
|
35
|
-
- test/loader_test.rb
|
36
|
-
- test/models.rb
|
37
|
-
- test/schema.rb
|
38
|
-
- test/watcher_test.rb
|
39
145
|
homepage: ''
|
40
146
|
licenses:
|
41
147
|
- Apache License Version 2.0
|
@@ -46,25 +152,18 @@ require_paths:
|
|
46
152
|
- lib
|
47
153
|
required_ruby_version: !ruby/object:Gem::Requirement
|
48
154
|
requirements:
|
49
|
-
- -
|
155
|
+
- - ">="
|
50
156
|
- !ruby/object:Gem::Version
|
51
157
|
version: '0'
|
52
158
|
required_rubygems_version: !ruby/object:Gem::Requirement
|
53
159
|
requirements:
|
54
|
-
- -
|
160
|
+
- - ">="
|
55
161
|
- !ruby/object:Gem::Version
|
56
162
|
version: '0'
|
57
163
|
requirements: []
|
58
164
|
rubyforge_project:
|
59
|
-
rubygems_version: 2.
|
165
|
+
rubygems_version: 2.2.2
|
60
166
|
signing_key:
|
61
167
|
specification_version: 4
|
62
168
|
summary: ''
|
63
|
-
test_files:
|
64
|
-
- test/active_record_collection_observation_test.rb
|
65
|
-
- test/database.yml
|
66
|
-
- test/helper.rb
|
67
|
-
- test/loader_test.rb
|
68
|
-
- test/models.rb
|
69
|
-
- test/schema.rb
|
70
|
-
- test/watcher_test.rb
|
169
|
+
test_files: []
|
data/.gitignore
DELETED
data/.travis.yml
DELETED
data/Gemfile
DELETED
data/Rakefile
DELETED
data/predictive_load.gemspec
DELETED
@@ -1,16 +0,0 @@
|
|
1
|
-
# encoding: utf-8
|
2
|
-
|
3
|
-
Gem::Specification.new do |gem|
|
4
|
-
gem.authors = ["Eric Chapweske"]
|
5
|
-
gem.email = ["eac@zendesk.com"]
|
6
|
-
gem.description = "Predictive loader"
|
7
|
-
gem.summary = %q{}
|
8
|
-
gem.homepage = ""
|
9
|
-
gem.license = "Apache License Version 2.0"
|
10
|
-
|
11
|
-
gem.files = `git ls-files`.split($\)
|
12
|
-
gem.executables = gem.files.grep(%r{^bin/}).map{ |f| File.basename(f) }
|
13
|
-
gem.test_files = gem.files.grep(%r{^(test|spec|features)/})
|
14
|
-
gem.name = "predictive_load"
|
15
|
-
gem.version = '0.1.2'
|
16
|
-
end
|
@@ -1,44 +0,0 @@
|
|
1
|
-
require_relative 'helper'
|
2
|
-
require 'predictive_load/watcher'
|
3
|
-
|
4
|
-
describe PredictiveLoad::ActiveRecordCollectionObservation do
|
5
|
-
|
6
|
-
describe "Relation#to_a" do
|
7
|
-
before do
|
8
|
-
user1 = User.create!(:name => "Rudolph")
|
9
|
-
user2 = User.create!(:name => "Santa")
|
10
|
-
end
|
11
|
-
|
12
|
-
after do
|
13
|
-
User.delete_all
|
14
|
-
end
|
15
|
-
|
16
|
-
describe "when a collection observer is specified" do
|
17
|
-
before do
|
18
|
-
ActiveRecord::Relation.collection_observer = PredictiveLoad::Watcher
|
19
|
-
end
|
20
|
-
|
21
|
-
it "observes the members of that collection" do
|
22
|
-
users = User.all
|
23
|
-
assert_equal 2, users.size
|
24
|
-
assert users.all? { |user| user.collection_observer }
|
25
|
-
end
|
26
|
-
|
27
|
-
end
|
28
|
-
|
29
|
-
describe "when a collection observer is not specified" do
|
30
|
-
before do
|
31
|
-
ActiveRecord::Relation.collection_observer = nil
|
32
|
-
end
|
33
|
-
|
34
|
-
it "does not observe the members of that collection" do
|
35
|
-
users = User.all
|
36
|
-
assert_equal 2, users.size, users.inspect
|
37
|
-
assert users.none? { |user| user.collection_observer }
|
38
|
-
end
|
39
|
-
|
40
|
-
end
|
41
|
-
|
42
|
-
end
|
43
|
-
|
44
|
-
end
|
data/test/database.yml
DELETED
data/test/helper.rb
DELETED
@@ -1,57 +0,0 @@
|
|
1
|
-
require 'rubygems'
|
2
|
-
require 'bundler/setup'
|
3
|
-
require 'minitest'
|
4
|
-
require 'minitest/spec'
|
5
|
-
require 'minitest/autorun'
|
6
|
-
require 'active_record'
|
7
|
-
require 'predictive_load'
|
8
|
-
require 'predictive_load/active_record_collection_observation'
|
9
|
-
|
10
|
-
ActiveRecord::Base.class_eval do
|
11
|
-
include PredictiveLoad::ActiveRecordCollectionObservation
|
12
|
-
end
|
13
|
-
|
14
|
-
database_config = YAML.load_file(File.join(File.dirname(__FILE__), 'database.yml'))
|
15
|
-
ActiveRecord::Base.establish_connection(database_config['test'])
|
16
|
-
ActiveRecord::Base.default_timezone = :utc
|
17
|
-
require_relative 'schema'
|
18
|
-
require_relative 'models'
|
19
|
-
|
20
|
-
def assert_queries(num = 1)
|
21
|
-
ActiveRecord::SQLCounter.log = []
|
22
|
-
yield
|
23
|
-
ensure
|
24
|
-
assert_equal num, ActiveRecord::SQLCounter.log.size, "#{ActiveRecord::SQLCounter.log.size} instead of #{num} queries were executed.#{ActiveRecord::SQLCounter.log.size == 0 ? '' : "\nQueries:\n#{ActiveRecord::SQLCounter.log.join("\n")}"}"
|
25
|
-
end
|
26
|
-
|
27
|
-
# Yanked from ActiveRecord tests
|
28
|
-
module ActiveRecord
|
29
|
-
class SQLCounter
|
30
|
-
cattr_accessor :ignored_sql
|
31
|
-
self.ignored_sql = [/^PRAGMA (?!(table_info))/, /^SELECT currval/, /^SELECT CAST/, /^SELECT @@IDENTITY/, /^SELECT @@ROWCOUNT/, /^SAVEPOINT/, /^ROLLBACK TO SAVEPOINT/, /^RELEASE SAVEPOINT/, /^SHOW max_identifier_length/, /^BEGIN/, /^COMMIT/]
|
32
|
-
|
33
|
-
# FIXME: this needs to be refactored so specific database can add their own
|
34
|
-
# ignored SQL. This ignored SQL is for Oracle.
|
35
|
-
ignored_sql.concat [/^select .*nextval/i, /^SAVEPOINT/, /^ROLLBACK TO/, /^\s*select .* from all_triggers/im]
|
36
|
-
|
37
|
-
cattr_accessor :log
|
38
|
-
self.log = []
|
39
|
-
|
40
|
-
attr_reader :ignore
|
41
|
-
|
42
|
-
def initialize(ignore = self.class.ignored_sql)
|
43
|
-
@ignore = ignore
|
44
|
-
end
|
45
|
-
|
46
|
-
def call(name, start, finish, message_id, values)
|
47
|
-
sql = values[:sql]
|
48
|
-
|
49
|
-
# FIXME: this seems bad. we should probably have a better way to indicate
|
50
|
-
# the query was cached
|
51
|
-
return if 'CACHE' == values[:name] || ignore.any? { |x| x =~ sql }
|
52
|
-
self.class.log << sql
|
53
|
-
end
|
54
|
-
end
|
55
|
-
|
56
|
-
ActiveSupport::Notifications.subscribe('sql.active_record', SQLCounter.new)
|
57
|
-
end
|
data/test/loader_test.rb
DELETED
@@ -1,155 +0,0 @@
|
|
1
|
-
require_relative 'helper'
|
2
|
-
require 'predictive_load/loader'
|
3
|
-
|
4
|
-
describe PredictiveLoad::Loader do
|
5
|
-
|
6
|
-
describe "A collection of records" do
|
7
|
-
before do
|
8
|
-
ActiveRecord::Relation.collection_observer = PredictiveLoad::Loader
|
9
|
-
# trigger schema lookup to avoid messing with query count assertions
|
10
|
-
Photo.columns
|
11
|
-
|
12
|
-
topic = Topic.create!(:title => "Sleigh repairs")
|
13
|
-
user1 = User.create!(:name => "Rudolph")
|
14
|
-
user2 = User.create!(:name => "Santa")
|
15
|
-
user1.emails.create!
|
16
|
-
comment1 = topic.comments.create!(:body => "meow", :user => user1)
|
17
|
-
comment2 = topic.comments.create!(:body => "Ho Ho ho", :user => user2)
|
18
|
-
end
|
19
|
-
|
20
|
-
after do
|
21
|
-
User.delete_all
|
22
|
-
Comment.delete_all
|
23
|
-
Topic.delete_all
|
24
|
-
Photo.delete_all
|
25
|
-
Email.delete_all
|
26
|
-
end
|
27
|
-
|
28
|
-
it "supports nested loading" do
|
29
|
-
# 3: User, Comment, Topic
|
30
|
-
assert_queries(3) do
|
31
|
-
User.all.each do |user|
|
32
|
-
user.comments.each { |comment| assert comment.topic }
|
33
|
-
end
|
34
|
-
end
|
35
|
-
end
|
36
|
-
|
37
|
-
describe "belongs_to" do
|
38
|
-
|
39
|
-
it "automatically preloads" do
|
40
|
-
comments = Comment.all
|
41
|
-
assert_equal 2, comments.size
|
42
|
-
assert_queries(1) do
|
43
|
-
comments.each { |comment| assert comment.user.name }
|
44
|
-
end
|
45
|
-
end
|
46
|
-
|
47
|
-
it "does not attempt to preload associations with proc conditions" do
|
48
|
-
comments = Comment.all
|
49
|
-
assert_equal 2, comments.size
|
50
|
-
assert_queries(2) do
|
51
|
-
comments.each { |comment| assert comment.user_by_proc.full_name }
|
52
|
-
end
|
53
|
-
end
|
54
|
-
|
55
|
-
end
|
56
|
-
|
57
|
-
describe "has_one" do
|
58
|
-
|
59
|
-
it "automatically preloads" do
|
60
|
-
users = User.all
|
61
|
-
assert_equal 2, users.size
|
62
|
-
|
63
|
-
assert_queries(1) do
|
64
|
-
users.each { |user| user.photo }
|
65
|
-
end
|
66
|
-
end
|
67
|
-
|
68
|
-
end
|
69
|
-
|
70
|
-
describe "has_many :through" do
|
71
|
-
|
72
|
-
it "automatically preloads" do
|
73
|
-
users = User.all
|
74
|
-
assert_equal 2, users.size
|
75
|
-
|
76
|
-
assert_queries(3) do
|
77
|
-
users.each do |user|
|
78
|
-
user.topics.each do |topic|
|
79
|
-
topic.comments.to_a
|
80
|
-
end
|
81
|
-
end
|
82
|
-
end
|
83
|
-
|
84
|
-
end
|
85
|
-
end
|
86
|
-
|
87
|
-
describe "has_and_belongs_to_many" do
|
88
|
-
|
89
|
-
it "automatically preloads" do
|
90
|
-
users = User.all
|
91
|
-
assert_equal 2, users.size
|
92
|
-
|
93
|
-
assert_queries(1) do
|
94
|
-
users.each { |user| user.emails.to_a }
|
95
|
-
end
|
96
|
-
|
97
|
-
end
|
98
|
-
end
|
99
|
-
|
100
|
-
describe "has_many" do
|
101
|
-
|
102
|
-
it "automatically prelaods" do
|
103
|
-
users = User.all
|
104
|
-
assert_equal 2, users.size
|
105
|
-
assert_queries(1) do
|
106
|
-
users.each { |user| user.comments.to_a }
|
107
|
-
end
|
108
|
-
end
|
109
|
-
|
110
|
-
it "preloads #length" do
|
111
|
-
users = User.all
|
112
|
-
assert_equal 2, users.size
|
113
|
-
assert_queries(1) do
|
114
|
-
users.each { |user| user.comments.length }
|
115
|
-
end
|
116
|
-
end
|
117
|
-
|
118
|
-
describe "unsupported behavior" do
|
119
|
-
it "does not preload when dynamically scoped" do
|
120
|
-
users = User.all
|
121
|
-
topic = Topic.first
|
122
|
-
assert_queries(2) do
|
123
|
-
users.each { |user| user.comments.by_topic(topic).to_a }
|
124
|
-
end
|
125
|
-
end
|
126
|
-
|
127
|
-
it "does not preload when staticly scoped" do
|
128
|
-
users = User.all
|
129
|
-
topic = Topic.first
|
130
|
-
assert_queries(2) do
|
131
|
-
users.each { |user| user.comments.recent.to_a }
|
132
|
-
end
|
133
|
-
end
|
134
|
-
|
135
|
-
|
136
|
-
it "does not preload #size" do
|
137
|
-
users = User.all
|
138
|
-
assert_queries(2) do
|
139
|
-
users.each { |user| user.comments.size }
|
140
|
-
end
|
141
|
-
end
|
142
|
-
|
143
|
-
it "does not preload first/last" do
|
144
|
-
users = User.all
|
145
|
-
assert_queries(2) do
|
146
|
-
users.each { |user| user.comments.first }
|
147
|
-
end
|
148
|
-
end
|
149
|
-
end
|
150
|
-
|
151
|
-
end
|
152
|
-
|
153
|
-
end
|
154
|
-
|
155
|
-
end
|
data/test/models.rb
DELETED
@@ -1,36 +0,0 @@
|
|
1
|
-
class User < ActiveRecord::Base
|
2
|
-
has_many :comments, :dependent => :destroy
|
3
|
-
has_many :topics, :through => :comments
|
4
|
-
has_one :photo
|
5
|
-
has_and_belongs_to_many :emails
|
6
|
-
|
7
|
-
def full_name
|
8
|
-
name
|
9
|
-
end
|
10
|
-
|
11
|
-
end
|
12
|
-
|
13
|
-
class Email < ActiveRecord::Base
|
14
|
-
end
|
15
|
-
|
16
|
-
class Topic < ActiveRecord::Base
|
17
|
-
has_many :comments, :dependent => :destroy
|
18
|
-
end
|
19
|
-
|
20
|
-
class Comment < ActiveRecord::Base
|
21
|
-
belongs_to :user
|
22
|
-
belongs_to :user_by_proc, :class_name => "User", :foreign_key => :user_id,
|
23
|
-
:conditions => proc { "1 = #{one}" }
|
24
|
-
belongs_to :topic
|
25
|
-
|
26
|
-
scope :by_topic, lambda { |topic| where(:topic_id => topic.id) }
|
27
|
-
scope :recent, order('updated_at desc')
|
28
|
-
|
29
|
-
def one
|
30
|
-
1
|
31
|
-
end
|
32
|
-
end
|
33
|
-
|
34
|
-
class Photo < ActiveRecord::Base
|
35
|
-
belongs_to :user
|
36
|
-
end
|
data/test/schema.rb
DELETED
@@ -1,37 +0,0 @@
|
|
1
|
-
ActiveRecord::Schema.define(:version => 1) do
|
2
|
-
|
3
|
-
drop_table(:users) rescue nil
|
4
|
-
drop_table(:emails) rescue nil
|
5
|
-
drop_table(:photos) rescue nil
|
6
|
-
drop_table(:topics) rescue nil
|
7
|
-
drop_table(:comments) rescue nil
|
8
|
-
drop_table(:emails_users) rescue nil
|
9
|
-
|
10
|
-
create_table(:users) do |t|
|
11
|
-
t.string :name, :null => false
|
12
|
-
end
|
13
|
-
|
14
|
-
create_table(:photos) do |t|
|
15
|
-
t.integer :user_id, :null => false
|
16
|
-
end
|
17
|
-
|
18
|
-
create_table(:emails_users) do |t|
|
19
|
-
t.integer :user_id
|
20
|
-
t.integer :email_id
|
21
|
-
end
|
22
|
-
|
23
|
-
create_table(:emails) do |t|
|
24
|
-
end
|
25
|
-
|
26
|
-
create_table(:topics) do |t|
|
27
|
-
t.string :title, :null => false
|
28
|
-
end
|
29
|
-
|
30
|
-
create_table(:comments) do |t|
|
31
|
-
t.string :body, :null => false
|
32
|
-
t.integer :topic_id, :null => false
|
33
|
-
t.integer :user_id, :null => false
|
34
|
-
t.timestamps
|
35
|
-
end
|
36
|
-
|
37
|
-
end
|
data/test/watcher_test.rb
DELETED
@@ -1,96 +0,0 @@
|
|
1
|
-
require_relative 'helper'
|
2
|
-
require 'predictive_load/watcher'
|
3
|
-
require 'logger'
|
4
|
-
|
5
|
-
describe PredictiveLoad::Watcher do
|
6
|
-
|
7
|
-
describe "A collection of records" do
|
8
|
-
before do
|
9
|
-
ActiveRecord::Relation.collection_observer = PredictiveLoad::Watcher
|
10
|
-
|
11
|
-
topic = Topic.create!(:title => "Sleigh repairs")
|
12
|
-
user1 = User.create!(:name => "Rudolph")
|
13
|
-
user2 = User.create!(:name => "Santa")
|
14
|
-
comment1 = topic.comments.create!(:body => "meow", :user => user1)
|
15
|
-
comment2 = topic.comments.create!(:body => "Ho Ho ho", :user => user2)
|
16
|
-
end
|
17
|
-
|
18
|
-
after do
|
19
|
-
User.delete_all
|
20
|
-
Comment.delete_all
|
21
|
-
Topic.delete_all
|
22
|
-
end
|
23
|
-
|
24
|
-
it "logs what the loader would have done" do
|
25
|
-
users = User.all
|
26
|
-
users[0].id = 1
|
27
|
-
users[1].id = 2
|
28
|
-
message = "predictive_load: detected n1 call on User#comments
|
29
|
-
predictive_load: expect to prevent 1 queries
|
30
|
-
predictive_load: would preload with: SELECT \"comments\".* FROM \"comments\" WHERE \"comments\".\"user_id\" IN (1, 2)
|
31
|
-
predictive_load: 0|0|0|SCAN TABLE comments (~100000 rows)
|
32
|
-
|
33
|
-
predictive_load: 0|0|0|EXECUTE LIST SUBQUERY 1
|
34
|
-
|
35
|
-
predictive_load: would have prevented all 1 queries
|
36
|
-
"
|
37
|
-
timing_pattern = /\d+\.\d+ms/
|
38
|
-
message.gsub!(timing_pattern, '')
|
39
|
-
assert_log(message, timing_pattern) do
|
40
|
-
users.each { |user| user.comments.to_a }
|
41
|
-
end
|
42
|
-
end
|
43
|
-
|
44
|
-
it "does not log :through association queries" do
|
45
|
-
users = User.all
|
46
|
-
message = "predictive_load: detected n1 call on User#topics
|
47
|
-
predictive_load: expect to prevent 1 queries
|
48
|
-
predictive_load: encountered :through association for topics. Requires loading records to generate query, so skipping for now.
|
49
|
-
predictive_load: would have prevented all 1 queries
|
50
|
-
"
|
51
|
-
|
52
|
-
timing_pattern = /\d+\.\d+ms/
|
53
|
-
message.gsub!(timing_pattern, '')
|
54
|
-
assert_log(message, timing_pattern) do
|
55
|
-
users.each { |user| user.topics.to_a }
|
56
|
-
end
|
57
|
-
|
58
|
-
end
|
59
|
-
|
60
|
-
it "does not blow up when preloading associations with proc conditions" do
|
61
|
-
log = StringIO.new
|
62
|
-
logger = build_logger(log)
|
63
|
-
ActiveRecord::Base.logger = logger
|
64
|
-
|
65
|
-
comments = Comment.all
|
66
|
-
assert_equal 2, comments.size
|
67
|
-
assert_queries(2) do
|
68
|
-
comments.each { |comment| assert comment.user_by_proc.full_name }
|
69
|
-
end
|
70
|
-
end
|
71
|
-
|
72
|
-
end
|
73
|
-
|
74
|
-
def assert_log(message, gsub_pattern)
|
75
|
-
original_logger = ActiveRecord::Base.logger
|
76
|
-
log = StringIO.new
|
77
|
-
logger = build_logger(log)
|
78
|
-
ActiveSupport::LogSubscriber.colorize_logging = false
|
79
|
-
ActiveRecord::Base.logger = logger
|
80
|
-
|
81
|
-
yield
|
82
|
-
result = log.string
|
83
|
-
result.gsub!(gsub_pattern, '')
|
84
|
-
assert_equal message, result
|
85
|
-
ensure
|
86
|
-
ActiveRecord::Base.logger = original_logger
|
87
|
-
end
|
88
|
-
|
89
|
-
def build_logger(log)
|
90
|
-
Logger.new(log).tap do |logger|
|
91
|
-
logger.level = Logger::Severity::INFO
|
92
|
-
logger.formatter = proc { |severity, datetime, progname, msg| "#{msg}\n" }
|
93
|
-
end
|
94
|
-
end
|
95
|
-
|
96
|
-
end
|