jit_preloader 0.2.4 → 1.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
  SHA256:
3
- metadata.gz: c697833105982c2f6ee8312618f5d32550c493c00c2b8ca76c6ca1895e387171
4
- data.tar.gz: c679aad87494ee39f689ef1e9d071897b78308c3b0c25847009703ec0712e741
3
+ metadata.gz: 2b1347f4a6e1db52e169d10cb83f6c365867f04882c873c9d6fa11c72553c385
4
+ data.tar.gz: 90fd4451da01e450c484ff23eec4ac08f9adbc955559adfa57972a639c37fefc
5
5
  SHA512:
6
- metadata.gz: 6eb1c49ec80fb2b9abf6cba22a38c55043de01e6d9138026b92ce000c460dd2dda20dc72d18ba94cd18b9393dea2cbc2fccfc4806ea6aa93b97bb323a697be9a
7
- data.tar.gz: 82b70f1ac6a12c6c87a980f06c1e85802fc5b16f566651c1945ed663d698b11ede6b0b0a4dbc2010b943d18d6955612c78f3281080ef8817e7e7fdc440979ec7
6
+ metadata.gz: 5fbf30e83a2f31e93831930eb4d4c09d4dc42a75f753f43da8b62082867e8810845f41da09dca4e8e840c2fdae569e1b95d6af565239c55f30b8474c88505cf7
7
+ data.tar.gz: caf85a19d5a50f325a828fca0c6c3f55adb56345c9fe76afea48abf366706baac3268ec6d6584946516af9f94a3cfc04346f5a886f920d73c8c1a7df6599839a
data/.gitignore CHANGED
@@ -43,7 +43,7 @@ build-iPhoneSimulator/
43
43
 
44
44
  # for a library or gem, you might want to ignore these files since the code is
45
45
  # intended to run in multiple environments; otherwise, check them in:
46
- # Gemfile.lock
46
+ Gemfile.lock
47
47
  # .ruby-version
48
48
  # .ruby-gemset
49
49
 
data/README.md CHANGED
@@ -208,6 +208,7 @@ end
208
208
  # SELECT * FROM contacts
209
209
  # SELECT * FROM countries WHERE name = "USA" LIMIT 1
210
210
  # SELECT "addresses".* FROM "addresses" WHERE "addresses"."country_id" = 10 AND "addresses"."contact_id" IN (1, 2, 3, ...)
211
+ ```
211
212
 
212
213
  ### Jit preloading globally across your application
213
214
 
@@ -18,7 +18,7 @@ Gem::Specification.new do |spec|
18
18
  spec.test_files = spec.files.grep(%r{^(test|spec|features)/})
19
19
  spec.require_paths = ["lib"]
20
20
 
21
- spec.add_dependency "activerecord", "> 4.2", "< 6"
21
+ spec.add_dependency "activerecord", "> 5.0", "< 7"
22
22
  spec.add_dependency "activesupport"
23
23
 
24
24
  spec.add_development_dependency "bundler"
data/lib/jit_preloader.rb CHANGED
@@ -8,16 +8,17 @@ require 'jit_preloader/active_record/base'
8
8
  require 'jit_preloader/active_record/relation'
9
9
  require 'jit_preloader/active_record/associations/collection_association'
10
10
  require 'jit_preloader/active_record/associations/singular_association'
11
- if Gem::Version.new(ActiveRecord::VERSION::STRING) < Gem::Version.new("5.2.2")
11
+ if Gem::Version.new(ActiveRecord::VERSION::STRING) >= Gem::Version.new("6.0.0")
12
+ require 'jit_preloader/active_record/associations/preloader/ar6_association'
13
+ elsif Gem::Version.new(ActiveRecord::VERSION::STRING) >= Gem::Version.new("5.2.2")
14
+ require 'jit_preloader/active_record/associations/preloader/ar5_association'
15
+ else
12
16
  require 'jit_preloader/active_record/associations/preloader/collection_association'
13
17
  require 'jit_preloader/active_record/associations/preloader/singular_association'
14
- else
15
- require 'jit_preloader/active_record/associations/preloader/association'
16
18
  end
17
19
  require 'jit_preloader/preloader'
18
20
 
19
21
  module JitPreloader
20
-
21
22
  def self.globally_enabled=(value)
22
23
  @enabled = value
23
24
  end
@@ -4,7 +4,7 @@ module JitPreloader
4
4
  def load_target
5
5
  was_loaded = loaded?
6
6
 
7
- if !loaded? && owner.persisted? && owner.jit_preloader
7
+ if !loaded? && owner.persisted? && owner.jit_preloader && (reflection.scope.nil? || reflection.scope.arity == 0)
8
8
  owner.jit_preloader.jit_preload(reflection.name)
9
9
  end
10
10
 
@@ -18,20 +18,13 @@ module JitPreloader
18
18
  # end
19
19
 
20
20
  def run(preloader)
21
- all_records = []
22
- records = load_records do |record|
23
- owner = owners_by_key[convert_key(record[association_key_name])]
24
- association = owner.association(reflection.name)
25
- association.set_inverse_instance(record)
26
- end
21
+ return unless (reflection.scope.nil? || reflection.scope.arity == 0) && klass.ancestors.include?(ActiveRecord::Base)
27
22
 
28
- owners.each do |owner|
29
- owned_records = records[convert_key(owner[owner_key_name])] || []
30
- all_records.concat(Array(owned_records)) if owner.jit_preloader || JitPreloader.globally_enabled?
31
- associate_records_to_owner(owner, owned_records)
23
+ super.tap do
24
+ if preloaded_records.any? && preloaded_records.none?(&:jit_preloader)
25
+ JitPreloader::Preloader.attach(preloaded_records) if owners.any?(&:jit_preloader) || JitPreloader.globally_enabled?
26
+ end
32
27
  end
33
-
34
- JitPreloader::Preloader.attach(all_records) if all_records.any?
35
28
  end
36
29
 
37
30
  # Original method:
@@ -54,6 +47,7 @@ module JitPreloader
54
47
  # the original copy so that we don't blow away in-memory changes.
55
48
  new_records = association.target.any? ? records - association.target : records
56
49
  association.target.concat(new_records)
50
+ association.loaded!
57
51
  else
58
52
  association.target ||= records.first unless records.empty?
59
53
  end
@@ -69,3 +63,4 @@ module JitPreloader
69
63
  end
70
64
 
71
65
  ActiveRecord::Associations::Preloader::Association.prepend(JitPreloader::PreloaderAssociation)
66
+ ActiveRecord::Associations::Preloader::ThroughAssociation.prepend(JitPreloader::PreloaderAssociation)
@@ -0,0 +1,57 @@
1
+ module JitPreloader
2
+ module PreloaderAssociation
3
+
4
+ # A monkey patch to ActiveRecord. The old method looked like the snippet
5
+ # below. Our changes here are that we remove records that are already
6
+ # part of the target, then attach all of the records to a new jit preloader.
7
+ #
8
+ # def run
9
+ # records = records_by_owner
10
+
11
+ # owners.each do |owner|
12
+ # associate_records_to_owner(owner, records[owner] || [])
13
+ # end if @associate
14
+
15
+ # self
16
+ # end
17
+
18
+ def run
19
+ return unless (reflection.scope.nil? || reflection.scope.arity == 0) && klass.ancestors.include?(ActiveRecord::Base)
20
+
21
+ super.tap do
22
+ if preloaded_records.any? && preloaded_records.none?(&:jit_preloader)
23
+ JitPreloader::Preloader.attach(preloaded_records) if owners.any?(&:jit_preloader) || JitPreloader.globally_enabled?
24
+ end
25
+ end
26
+ end
27
+
28
+ # Original method:
29
+ # def associate_records_to_owner(owner, records)
30
+ # association = owner.association(reflection.name)
31
+ # if reflection.collection?
32
+ # association.target = records
33
+ # else
34
+ # association.target = records.first
35
+ # end
36
+ # end
37
+ def associate_records_to_owner(owner, records)
38
+ association = owner.association(reflection.name)
39
+ if reflection.collection?
40
+ new_records = association.target.any? ? records - association.target : records
41
+ association.target.concat(new_records)
42
+ association.loaded!
43
+ else
44
+ association.target = records.first
45
+ end
46
+ end
47
+
48
+ def build_scope
49
+ super.tap do |scope|
50
+ scope.jit_preload! if owners.any?(&:jit_preloader) || JitPreloader.globally_enabled?
51
+ end
52
+ end
53
+ end
54
+ end
55
+
56
+ ActiveRecord::Associations::Preloader::Association.prepend(JitPreloader::PreloaderAssociation)
57
+ ActiveRecord::Associations::Preloader::ThroughAssociation.prepend(JitPreloader::PreloaderAssociation)
@@ -4,7 +4,7 @@ module JitPreloader
4
4
  def load_target
5
5
  was_loaded = loaded?
6
6
 
7
- if !loaded? && owner.persisted? && owner.jit_preloader
7
+ if !loaded? && owner.persisted? && owner.jit_preloader && (reflection.scope.nil? || reflection.scope.arity == 0)
8
8
  owner.jit_preloader.jit_preload(reflection.name)
9
9
  end
10
10
 
@@ -18,44 +18,66 @@ module JitPreloadExtension
18
18
  end
19
19
  end
20
20
 
21
- def preload_scoped_relation(name:, base_association:, preload_scope: nil)
22
- return jit_preload_scoped_relations[name] if jit_preload_scoped_relations&.key?(name)
21
+ if Gem::Version.new(ActiveRecord::VERSION::STRING) >= Gem::Version.new("6.0.0")
22
+ def preload_scoped_relation(name:, base_association:, preload_scope: nil)
23
+ return jit_preload_scoped_relations[name] if jit_preload_scoped_relations&.key?(name)
24
+
25
+ records = jit_preloader&.records || [self]
26
+
27
+ preloader_association = ActiveRecord::Associations::Preloader.new.preload(
28
+ records,
29
+ base_association,
30
+ preload_scope
31
+ ).first
32
+
33
+ records.each do |record|
34
+ record.jit_preload_scoped_relations ||= {}
35
+ association = record.association(base_association)
36
+ record.jit_preload_scoped_relations[name] = preloader_association.records_by_owner[record]
37
+ end
38
+
39
+ jit_preload_scoped_relations[name]
40
+ end
41
+ else
42
+ def preload_scoped_relation(name:, base_association:, preload_scope: nil)
43
+ return jit_preload_scoped_relations[name] if jit_preload_scoped_relations&.key?(name)
44
+
45
+ records = jit_preloader&.records || [self]
46
+ previous_association_values = {}
47
+
48
+ records.each do |record|
49
+ association = record.association(base_association)
50
+ if association.loaded?
51
+ previous_association_values[record] = association.target
52
+ association.reset
53
+ end
54
+ end
23
55
 
24
- records = jit_preloader&.records || [self]
25
- previous_association_values = {}
56
+ ActiveRecord::Associations::Preloader.new.preload(
57
+ records,
58
+ base_association,
59
+ preload_scope
60
+ )
26
61
 
27
- records.each do |record|
28
- association = record.association(base_association)
29
- if association.loaded?
30
- previous_association_values[record] = association.target
62
+ records.each do |record|
63
+ record.jit_preload_scoped_relations ||= {}
64
+ association = record.association(base_association)
65
+ record.jit_preload_scoped_relations[name] = association.target
31
66
  association.reset
67
+ if previous_association_values.key?(record)
68
+ association.target = previous_association_values[record]
69
+ end
32
70
  end
33
- end
34
71
 
35
- ActiveRecord::Associations::Preloader.new.preload(
36
- records,
37
- base_association,
38
- preload_scope
39
- )
40
-
41
- records.each do |record|
42
- record.jit_preload_scoped_relations ||= {}
43
- association = record.association(base_association)
44
- record.jit_preload_scoped_relations[name] = association.target
45
- association.reset
46
- if previous_association_values.key?(record)
47
- association.target = previous_association_values[record]
48
- end
72
+ jit_preload_scoped_relations[name]
49
73
  end
50
-
51
- jit_preload_scoped_relations[name]
52
74
  end
53
75
 
54
76
  def self.prepended(base)
55
77
  class << base
56
78
  delegate :jit_preload, to: :all
57
79
 
58
- def has_many_aggregate(assoc, name, aggregate, field, default: 0)
80
+ def has_many_aggregate(assoc, name, aggregate, field, table_alias_name: nil, default: 0)
59
81
  method_name = "#{assoc}_#{name}"
60
82
 
61
83
  define_method(method_name) do |conditions={}|
@@ -77,8 +99,8 @@ module JitPreloadExtension
77
99
  association_scope = association_scope.instance_exec(&reflection.scope).reorder(nil) if reflection.scope
78
100
 
79
101
  # If the query uses an alias for the association, use that instead of the table name
80
- table_alias_name = association_scope.references_values.first
81
- table_reference = table_alias_name || aggregate_association.table_name
102
+ table_reference = table_alias_name
103
+ table_reference ||= association_scope.references_values.first || aggregate_association.table_name
82
104
 
83
105
  conditions[table_reference] = { aggregate_association.foreign_key => primary_ids }
84
106
 
@@ -1,3 +1,3 @@
1
1
  module JitPreloader
2
- VERSION = "0.2.4"
2
+ VERSION = "1.0.2"
3
3
  end
@@ -1,4 +1,4 @@
1
- require 'spec_helper'
1
+ require "spec_helper"
2
2
 
3
3
  RSpec.describe JitPreloader::Preloader do
4
4
  let!(:contact1) do
@@ -106,6 +106,19 @@ RSpec.describe JitPreloader::Preloader do
106
106
  expect(contact_books.first.children).to include(child1, child2, child3)
107
107
  end
108
108
  end
109
+
110
+ context "when preloading an aggregate for a child model scoped by another join table" do
111
+ let!(:contact_book) { ContactBook.create(name: "The Yellow Pages") }
112
+ let!(:contact1) { Company.create(name: "Without Email", contact_book: contact_book) }
113
+ let!(:contact2) { Company.create(name: "With Blank Email", email_address: EmailAddress.new(address: ""), contact_book: contact_book) }
114
+ let!(:contact3) { Company.create(name: "With Email", email_address: EmailAddress.new(address: "a@a.com"), contact_book: contact_book) }
115
+
116
+ it "can handle queries" do
117
+ contact_books = ContactBook.jit_preload.to_a
118
+ expect(contact_books.first.companies_with_blank_email_address_count).to eq 1
119
+ expect(contact_books.first.companies_with_blank_email_address).to eq [contact2]
120
+ end
121
+ end
109
122
  end
110
123
 
111
124
  context "when preloading an aggregate as polymorphic" do
@@ -141,6 +154,16 @@ RSpec.describe JitPreloader::Preloader do
141
154
  expect(c.contacts_count).to eql contact_owner_counts[i]
142
155
  end
143
156
  end
157
+
158
+ context "when a record has a polymorphic association type that's not an ActiveRecord" do
159
+ before do
160
+ contact1.update!(contact_owner_type: "NilClass", contact_owner_id: nil)
161
+ end
162
+
163
+ it "doesn't die while trying to load the association" do
164
+ expect(Contact.jit_preload.map(&:contact_owner)).to eq [nil, ContactOwner.first, Address.first]
165
+ end
166
+ end
144
167
  end
145
168
  end
146
169
 
@@ -179,6 +202,22 @@ RSpec.describe JitPreloader::Preloader do
179
202
  end
180
203
  end
181
204
 
205
+ context "when accessing an association with a scope that has a parameter" do
206
+ let!(:contact_book) { ContactBook.create(name: "The Yellow Pages") }
207
+ let!(:contact) { Contact.create(name: "Contact", contact_book: contact_book) }
208
+ let!(:company1) { Company.create(name: "Company1", contact_book: contact_book) }
209
+
210
+ it "is unable to be preloaded" do
211
+ ActiveSupport::Notifications.subscribed(callback, "n_plus_one_query") do
212
+ ContactBook.all.jit_preload.each do |contact_book|
213
+ expect(contact_book.contacts_with_scope.to_a).to eql [company1, contact]
214
+ end
215
+ end
216
+
217
+ expect(source_map).to eql(Hash[contact_book, [:contacts_with_scope]])
218
+ end
219
+ end
220
+
182
221
  context "when preloading an aggregate on a polymorphic has_many through relationship" do
183
222
  let(:contact_owner_addresses_counts) { [3] }
184
223
 
@@ -1,5 +1,6 @@
1
1
  class ContactBook < ActiveRecord::Base
2
2
  has_many :contacts
3
+ has_many :contacts_with_scope, ->(_contact_book) { desc }, class_name: "Contact", foreign_key: :contact_book_id
3
4
  has_many :employees, through: :contacts
4
5
 
5
6
  has_many :companies
@@ -12,6 +13,9 @@ class ContactBook < ActiveRecord::Base
12
13
  has_many_aggregate :employees, :count, :count, "*"
13
14
  has_many_aggregate :company_employees, :count, :count, "*"
14
15
  has_many_aggregate :children, :count, :count, "*"
16
+
17
+ has_many :companies_with_blank_email_address, -> { joins(:email_address).where(email_addresses: { address: "" }) }, class_name: "Company"
18
+ has_many_aggregate :companies_with_blank_email_address, :count, :count, "*", table_alias_name: "contacts"
15
19
  end
16
20
 
17
21
  class Contact < ActiveRecord::Base
@@ -26,6 +30,8 @@ class Contact < ActiveRecord::Base
26
30
  has_many_aggregate :addresses, :max_street_length, :maximum, "LENGTH(street)"
27
31
  has_many_aggregate :phone_numbers, :count, :count, "id"
28
32
  has_many_aggregate :addresses, :count, :count, "*"
33
+
34
+ scope :desc, ->{ order(id: :desc) }
29
35
  end
30
36
 
31
37
  class Company < Contact
metadata CHANGED
@@ -1,14 +1,14 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: jit_preloader
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.2.4
4
+ version: 1.0.2
5
5
  platform: ruby
6
6
  authors:
7
7
  - Kyle d'Oliveira
8
8
  autorequire:
9
9
  bindir: bin
10
10
  cert_chain: []
11
- date: 2020-04-02 00:00:00.000000000 Z
11
+ date: 2021-03-03 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: activerecord
@@ -16,20 +16,20 @@ dependencies:
16
16
  requirements:
17
17
  - - ">"
18
18
  - !ruby/object:Gem::Version
19
- version: '4.2'
19
+ version: '5.0'
20
20
  - - "<"
21
21
  - !ruby/object:Gem::Version
22
- version: '6'
22
+ version: '7'
23
23
  type: :runtime
24
24
  prerelease: false
25
25
  version_requirements: !ruby/object:Gem::Requirement
26
26
  requirements:
27
27
  - - ">"
28
28
  - !ruby/object:Gem::Version
29
- version: '4.2'
29
+ version: '5.0'
30
30
  - - "<"
31
31
  - !ruby/object:Gem::Version
32
- version: '6'
32
+ version: '7'
33
33
  - !ruby/object:Gem::Dependency
34
34
  name: activesupport
35
35
  requirement: !ruby/object:Gem::Requirement
@@ -154,14 +154,14 @@ files:
154
154
  - ".gitignore"
155
155
  - ".rspec"
156
156
  - Gemfile
157
- - Gemfile.lock
158
157
  - LICENSE
159
158
  - README.md
160
159
  - Rakefile
161
160
  - jit_preloader.gemspec
162
161
  - lib/jit_preloader.rb
163
162
  - lib/jit_preloader/active_record/associations/collection_association.rb
164
- - lib/jit_preloader/active_record/associations/preloader/association.rb
163
+ - lib/jit_preloader/active_record/associations/preloader/ar5_association.rb
164
+ - lib/jit_preloader/active_record/associations/preloader/ar6_association.rb
165
165
  - lib/jit_preloader/active_record/associations/preloader/collection_association.rb
166
166
  - lib/jit_preloader/active_record/associations/preloader/singular_association.rb
167
167
  - lib/jit_preloader/active_record/associations/singular_association.rb
@@ -193,7 +193,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement
193
193
  - !ruby/object:Gem::Version
194
194
  version: '0'
195
195
  requirements: []
196
- rubygems_version: 3.0.6
196
+ rubygems_version: 3.0.3
197
197
  signing_key:
198
198
  specification_version: 4
199
199
  summary: Tool to understand N+1 queries and to remove them
data/Gemfile.lock DELETED
@@ -1,66 +0,0 @@
1
- PATH
2
- remote: .
3
- specs:
4
- jit_preloader (0.2.3)
5
- activerecord (> 4.2, < 6)
6
- activesupport
7
-
8
- GEM
9
- remote: https://rubygems.org/
10
- specs:
11
- activemodel (5.2.4.2)
12
- activesupport (= 5.2.4.2)
13
- activerecord (5.2.4.2)
14
- activemodel (= 5.2.4.2)
15
- activesupport (= 5.2.4.2)
16
- arel (>= 9.0)
17
- activesupport (5.2.4.2)
18
- concurrent-ruby (~> 1.0, >= 1.0.2)
19
- i18n (>= 0.7, < 2)
20
- minitest (~> 5.1)
21
- tzinfo (~> 1.1)
22
- arel (9.0.0)
23
- byebug (9.0.6)
24
- concurrent-ruby (1.1.6)
25
- database_cleaner (1.5.3)
26
- db-query-matchers (0.10.0)
27
- activesupport (>= 4.0, < 7)
28
- rspec (~> 3.0)
29
- diff-lcs (1.2.5)
30
- i18n (1.8.2)
31
- concurrent-ruby (~> 1.0)
32
- minitest (5.14.0)
33
- rake (13.0.1)
34
- rspec (3.5.0)
35
- rspec-core (~> 3.5.0)
36
- rspec-expectations (~> 3.5.0)
37
- rspec-mocks (~> 3.5.0)
38
- rspec-core (3.5.4)
39
- rspec-support (~> 3.5.0)
40
- rspec-expectations (3.5.0)
41
- diff-lcs (>= 1.2.0, < 2.0)
42
- rspec-support (~> 3.5.0)
43
- rspec-mocks (3.5.0)
44
- diff-lcs (>= 1.2.0, < 2.0)
45
- rspec-support (~> 3.5.0)
46
- rspec-support (3.5.0)
47
- sqlite3 (1.3.12)
48
- thread_safe (0.3.6)
49
- tzinfo (1.2.6)
50
- thread_safe (~> 0.1)
51
-
52
- PLATFORMS
53
- ruby
54
-
55
- DEPENDENCIES
56
- bundler
57
- byebug
58
- database_cleaner
59
- db-query-matchers
60
- jit_preloader!
61
- rake (~> 13.0)
62
- rspec
63
- sqlite3
64
-
65
- BUNDLED WITH
66
- 2.0.1